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
| Component | Location | Purpose |
|---|---|---|
| PSI Logon (NFC Launcher) | app/ | Android app — orchestrates auth, NFC, and broker sign-in |
| TAP Bridge | functions/TapBridge/ | Azure Function — creates TAPs via Graph API |
| TapInjectorService | app/.../service/ | Accessibility service — injects TAP into broker password field |
| Microsoft Managed Home Screen | Intune config | Enterprise kiosk launcher — app grid for operators |
| Microsoft Authenticator | Device | Broker 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
| Step | What Happens |
|---|---|
| Sign in | Open PSI Logon from MHS → enter PIN → tap badge → green check with name (2s) → auto-return to MHS |
| Use apps | From MHS, tap Teams, PS MES, or Edge — SSO via PRT, no additional login |
| Sign out | Open PSI Logon → tap Sign Out → return to MHS |
| Wrong PIN | Red “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:
getCurrentAccountAsyncfires bothonAccountLoadedandonAccountChangedon shared devices — guarded with aresumedflag 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-playaction
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:
| Setting | Value | Purpose |
|---|---|---|
enable_mhs_signin | false | No MHS sign-in — PSI Logon handles auth |
exit_lock_task_mode_code | 1234 | IT admin escape hatch |
screen_orientation | 1 | Portrait lock |
grid_size | 4;2 | 4 rows x 2 columns |
icon_size | 2 | Large icons for gloved hands |
lock_home_screen | true | Operators can’t rearrange |
theme | 2 | Dark theme for shop floor |
top_bar_user_name_style | Show both | Name + email in top bar |
enable_show_organization_logo | true | Square “Pro” logo |
App Allowlist
| App | Package |
|---|---|
| PSI Logon | com.psi.nfclauncher |
| PS MES | com.progressivesurface.mes |
| Microsoft Teams | com.microsoft.teams |
| Microsoft Edge | com.microsoft.emmx |
Azure Resources
| Resource | Value |
|---|---|
| Function App | psi-tap-bridge |
| Resource Group | PS-WEBAPPS |
| App Service Plan | asp-erp-migration-tool (B3 Linux, shared) |
| Runtime | .NET 8 Isolated (DOTNET-ISOLATED|8.0) |
| Private Endpoint | psi-tap-bridge-pe (10.160.140.6, PS-ProdData) |
| Public Access | Disabled |
| Storage | psargostorage (shared, for Azure Functions host) |
| Managed Identity | System-assigned (Key Vault access) |
Entra ID Configuration
| Resource | Value |
|---|---|
| App Registration | PSI TAP Bridge |
| Application (Client) ID | 11d892e3-e20b-4fb2-aef7-6f9b37d02cb6 |
| Service Principal ID | c9e9f0f2-739d-4526-8315-48bd0d942975 |
| Client Secret | Stored in Key Vault (TapBridge--ClientSecret) |
| Graph Permission | UserAuthenticationMethod.ReadWrite.All (application) |
| Directory Role | Authentication Administrator |
| TAP Policy | Enabled, 60-minute lifetime, single-use |
DNS Records
| Record | IP | Zone |
|---|---|---|
psi-tap-bridge | 10.160.140.6 | privatelink.azurewebsites.net |
psi-tap-bridge.scm | 10.160.140.6 | privatelink.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.
| Setting | Value |
|---|---|
| Function Key | Stored in Azure (retrieve via portal or az functionapp keys list) |
| Network | Private endpoint only — device must be on corporate network |
Secrets Management
All secrets use Key Vault references — no plain text in app settings:
| App Setting | Key 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:
| Group | Object ID | Purpose |
|---|---|---|
| DG - 125 - Process Services | 4e94ac7d-7993-451c-a079-3983edfaa058 | Production floor workers |
| TAP Test | be051cc3-4600-4945-9456-15c662a5d5f5 | Testing/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:
| Status | Meaning |
|---|---|
| 400 | Missing or empty upn |
| 403 | User not in an allowed group |
| 500 | Graph API error (check logs) |
| 502 | Graph 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.zipApp Settings
| Setting | Value |
|---|---|
FUNCTIONS_EXTENSION_VERSION | ~4 |
FUNCTIONS_WORKER_RUNTIME | dotnet-isolated |
SCM_DO_BUILD_DURING_DEPLOYMENT | false |
ENTRA_TENANT_ID | a83ae943-0a50-49cc-83c3-479b7a44b7fb |
ENTRA_CLIENT_ID | 11d892e3-e20b-4fb2-aef7-6f9b37d02cb6 |
ENTRA_CLIENT_SECRET | Key Vault reference |
AzureWebJobsStorage | Key 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_urlandtap_bridge_keydelivered viaRestrictionsManager - 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_infor PSI Logon
Last updated: March 16, 2026