NFC TAP Bridge

Passwordless NFC badge sign-in for shared shop floor devices — FIDO2 authentication + Temporary Access Pass + Microsoft Managed Home Screen.


Overview

The NFC TAP Bridge system enables passwordless sign-in for shared Android devices (Zebra TC52 scanners) on the manufacturing floor. Workers tap their NFC security badge, enter a PIN, and the system authenticates them via FIDO2 → creates a Temporary Access Pass → establishes a Primary Refresh Token (PRT) through the Microsoft Authenticator broker. This gives SSO across Teams, Edge, PS MES, and other Microsoft apps.

Repository: psi-nfc-launcher Current Version: v2.14.0 (PSI Logon) Package Name: com.psi.nfclauncher

Components

ComponentLocationPurpose
PSI Logon (NFC Launcher)app/Android app — orchestrates auth, NFC, and broker sign-in
TAP Bridgefunctions/TapBridge/Azure Function — creates TAPs via Graph API
TapInjectorServiceapp/.../service/Accessibility service — injects TAP into broker password field
Microsoft Managed Home ScreenIntune configEnterprise kiosk launcher — app grid for operators
Microsoft AuthenticatorDeviceBroker app in shared device mode — issues PRT

Architecture

Microsoft Managed Home Screen (kiosk launcher, sign-in disabled)
    |
    | User taps "PSI Logon" icon
    v
PSI Logon App → Login screen → "Sign In" button
    |
    | 1. User enters FIDO2 PIN
    | 2. User taps NFC security badge
    v
FIDO2 WebAuthn Auth (WebView → Entra ID → CTAP2 over NFC)
    |
    | Auth code returned via msauth:// redirect
    v
Token Exchange (direct HTTP to Entra token endpoint)
    |
    | POST /api/create-tap?code=<function-key>
    | Body: { "upn": "user@progressivesurface.com" }
    v
Azure Function (psi-tap-bridge)
    |
    | 1. Verify user is in allowed Entra group
    | 2. Create TAP via Microsoft Graph API
    v
Returns: { "tap": "A9eM#FKy", "lifetime": 60 }
    |
    | TapInjectorService injects TAP into Authenticator broker
    | (handles passkey fallback page automatically)
    v
Microsoft Authenticator → PRT established → SSO for all apps
    |
    | Green check + user name (2 seconds)
    v
Auto-finish back to MHS home screen

PSI Logon (Android App)

The PSI Logon app (formerly “PSI Launcher”) handles NFC badge authentication and session management. It works alongside Microsoft Managed Home Screen as a dedicated auth utility — not a launcher.

Operator Experience

StepWhat Happens
Sign inOpen PSI Logon from MHS → enter PIN → tap badge → green check with name (2s) → auto-return to MHS
Use appsFrom MHS, tap Teams, PS MES, or Edge — SSO via PRT, no additional login
Sign outOpen PSI Logon → tap Sign Out → return to MHS
Wrong PINRed “Incorrect PIN” banner shown, PIN retry dialog opens automatically
Badge lost contact”Badge lost contact. Hold your badge steady and try again.”

Key Technical Details

  • FIDO2 via WebView: Desktop user-agent WebView loads Entra authorize URL, JavaScript bridge intercepts navigator.credentials.get() calls for CTAP2 over NFC
  • Passkey fallback: If Android Credential Manager triggers a failed passkey flow during broker sign-in, the app automatically clicks “Use my password” and re-injects the TAP
  • MSAL shared device mode: getCurrentAccountAsync fires both onAccountLoaded and onAccountChanged on shared devices — guarded with a resumed flag to prevent double-resume crash
  • Session management: 12-hour idle timeout (safety net), MHS handles session lifecycle
  • CI/CD: GitHub Actions publishes signed APK to Managed Google Play via r0adkll/upload-google-play action

Managed Home Screen (MHS)

Microsoft Managed Home Screen (com.microsoft.launcher.enterprise) runs as the kiosk launcher on TC52 devices. Sign-in is disabled — MHS is a “dumb shell” that shows the app grid.

Configuration

Configured via Intune App Configuration Policy using Managed Google Play JSON format:

SettingValuePurpose
enable_mhs_signinfalseNo MHS sign-in — PSI Logon handles auth
exit_lock_task_mode_code1234IT admin escape hatch
screen_orientation1Portrait lock
grid_size4;24 rows x 2 columns
icon_size2Large icons for gloved hands
lock_home_screentrueOperators can’t rearrange
theme2Dark theme for shop floor
top_bar_user_name_styleShow bothName + email in top bar
enable_show_organization_logotrueSquare “Pro” logo

App Allowlist

AppPackage
PSI Logoncom.psi.nfclauncher
PS MEScom.progressivesurface.mes
Microsoft Teamscom.microsoft.teams
Microsoft Edgecom.microsoft.emmx

Azure Resources

ResourceValue
Function Apppsi-tap-bridge
Resource GroupPS-WEBAPPS
App Service Planasp-erp-migration-tool (B3 Linux, shared)
Runtime.NET 8 Isolated (DOTNET-ISOLATED|8.0)
Private Endpointpsi-tap-bridge-pe (10.160.140.6, PS-ProdData)
Public AccessDisabled
Storagepsargostorage (shared, for Azure Functions host)
Managed IdentitySystem-assigned (Key Vault access)

Entra ID Configuration

ResourceValue
App RegistrationPSI TAP Bridge
Application (Client) ID11d892e3-e20b-4fb2-aef7-6f9b37d02cb6
Service Principal IDc9e9f0f2-739d-4526-8315-48bd0d942975
Client SecretStored in Key Vault (TapBridge--ClientSecret)
Graph PermissionUserAuthenticationMethod.ReadWrite.All (application)
Directory RoleAuthentication Administrator
TAP PolicyEnabled, 60-minute lifetime, single-use

DNS Records

RecordIPZone
psi-tap-bridge10.160.140.6privatelink.azurewebsites.net
psi-tap-bridge.scm10.160.140.6privatelink.azurewebsites.net

AD-integrated A records on PS-AZ-DC01 for both.


Security

Authentication

The function uses Function-level key authentication (AuthorizationLevel.Function). The Android app must include the function key as a query parameter.

SettingValue
Function KeyStored in Azure (retrieve via portal or az functionapp keys list)
NetworkPrivate endpoint only — device must be on corporate network

Secrets Management

All secrets use Key Vault references — no plain text in app settings:

App SettingKey Vault Secret
ENTRA_CLIENT_SECRET@Microsoft.KeyVault(VaultName=ps-certificates-kv;SecretName=TapBridge--ClientSecret)
AzureWebJobsStorage@Microsoft.KeyVault(VaultName=ps-certificates-kv;SecretName=TapBridge--StorageConnection)

Group Restriction

The function only creates TAPs for users who are members of specific Entra groups:

GroupObject IDPurpose
DG - 125 - Process Services4e94ac7d-7993-451c-a079-3983edfaa058Production floor workers
TAP Testbe051cc3-4600-4945-9456-15c662a5d5f5Testing/development

Users not in either group receive a 403 Forbidden response. Group IDs are hardcoded in CreateTap.cs.


API

POST /api/create-tap

Creates a one-time Temporary Access Pass for the specified user.

Auth: Function key required (?code=<key>)

Request:

{
  "upn": "adevereaux@progressivesurface.com"
}

Success (200):

{
  "tap": "A9eM#FKy",
  "lifetime": 60
}

Errors:

StatusMeaning
400Missing or empty upn
403User not in an allowed group
500Graph API error (check logs)
502Graph returned null TAP

Deployment

Build and Deploy

The function runs on Linux but is built on Windows. Key requirement: use 7-Zip (not PowerShell Compress-Archive) to create the deployment zip — PowerShell creates zips with backslash path separators that Linux Kudu cannot extract.

cd functions/TapBridge
dotnet publish -c Release -r linux-x64 --self-contained false -o ./publish
cd publish
"C:/Program Files/7-Zip/7z.exe" a -tzip ../deploy.zip .
cd ..
az functionapp deployment source config-zip \
  --resource-group PS-WEBAPPS \
  --name psi-tap-bridge \
  --src deploy.zip

App Settings

SettingValue
FUNCTIONS_EXTENSION_VERSION~4
FUNCTIONS_WORKER_RUNTIMEdotnet-isolated
SCM_DO_BUILD_DURING_DEPLOYMENTfalse
ENTRA_TENANT_IDa83ae943-0a50-49cc-83c3-479b7a44b7fb
ENTRA_CLIENT_ID11d892e3-e20b-4fb2-aef7-6f9b37d02cb6
ENTRA_CLIENT_SECRETKey Vault reference
AzureWebJobsStorageKey Vault reference

Infrastructure (ARM Template)

The function app was created via ARM template (functions/TapBridge/arm-deploy.json) because az functionapp create does not support --public-network-access false, and the subscription has a Deny policy on public App Services.

Known Issues

  • PowerShell Compress-Archive: Creates zips with backslash separators — Linux Kudu extracts only root-level files (e.g., host.json). Use 7-Zip instead.
  • Consumption plan: Does not support private endpoints. Must use a dedicated App Service plan.
  • AzureWebJobsStorage: Required even for HTTP-only triggers on dedicated plans — the function host won’t discover functions without it.
  • WEBSITE_CONTENTSHARE / WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: Not needed on dedicated plans and can cause mount failures if storage isn’t VNet-accessible. Remove them.

TODO

  • Rotate client secret (original is in git history) and update Key Vault
  • Scope Authentication Administrator role via Administrative Unit (currently directory-wide, mitigated by code-level group check)
  • Set up CI/CD GitHub Actions workflow — done, publishes to Managed Google Play
  • Configure Android app with function URL and key via Intune managed restrictions — done, tap_bridge_url and tap_bridge_key delivered via RestrictionsManager
  • Validate OEMConfig ACCESSIBILITY_SERVICE_ACCESS grant on fleet after managed Play Store install
  • Eventually re-enable MHS sign-in with configure_app_access_without_sign_in for PSI Logon

Last updated: March 16, 2026