Implementing Human-in-the-Loop (HITL) Authentication with Zitadel and Web Push
How to safely grant elevated permissions to background bots using on-demand human verification
Architectural Overview
Traditional HITL flows often require users to manually copy and paste verification codes between screens. By combining Progressive Web App (PWA) push notifications with Zitadel's API, we can eliminate the manual steps entirely.
Here is the lifecycle of a secure HITL request:
-
Device Registration: A human user logs into the main application and enables PWA notifications. The browser generates a secure Web Push subscription object, which is then saved to the user's Zitadel Profile Metadata.
-
Access Request: A background bot needs to perform an action on behalf of that user, for example, to make a purchase online. It initiates the OAuth2 Device Code flow via Zitadel.
-
The Notification: The bot reads the user's Push Subscription from Zitadel and sends a native OS notification containing the authorization link.
-
Human Approval: The user clicks the native OS notification on their device, which opens Zitadel's login screen with the device code pre-filled. If the user has a valid SSO session, Zitadel will only prompt the user to approve the request.
-
Token Acquisition: The bot, which has been polling Zitadel in the background, detects the successful login, receives the secure JWT, and resumes its task.
Prerequisites in Zitadel
To implement this architecture, you will need to configure three specific entities in your Zitadel project:
-
A Web Application (OIDC): For the human user to log into your app (e.g., using NextAuth.js).
-
A Native Application: For the bot to initiate the Device Code flow. Native apps do not require client secrets, making them perfect for public or distributed bots.
-
A Service User & PAT: A machine user with a Personal Access Token (PAT) that has permissions to read and write User Metadata via the Zitadel Management API.
Phase 1: Binding the Device to the Identity
For the bot to know where to send the notification, the human operator must first bind their physical browser/device to their Zitadel identity.
-
The user logs into your web portal, for example, the online store that allow a bot to make purchases automatically on the background.
-
The browser requests permission to show notifications using the
NotificationandServiceWorkerAPIs. -
The frontend generates a secure subscription object using VAPID keys.
-
The backend uses the Zitadel
v2.UserService/SetUserMetadataendpoint to securely store this JSON subscription object against the user'suserIdunder a key likepush_subscription.
Phase 2: The Bot Initiates the Flow
When the automated process reaches a point where it requires human approval:
-
Read Metadata: The bot fetches the target user's
push_subscriptionfrom Zitadel using the Service User PAT. -
Request Device Code: The bot sends a POST request to Zitadel's
/oauth/v2/device_authorizationendpoint using the Native App Client ID. Zitadel returns adevice_code, auser_code, and averification_uri_complete. -
Fire Web Push: The bot utilizes a library (like
web-pushin Node.js) to push theverification_uri_completepayload directly to the user's registered browser endpoint. -
Start Polling: The bot begins polling Zitadel's
/oauth/v2/tokenendpoint at the required interval, waiting for the user to approve the request.
Phase 3: Frictionless User Experience
Because the bot sent the verification_uri_complete (which includes the code embedded in the URL), the user experience is entirely frictionless.
-
The user receives a native OS popup (Mac, Windows, iOS, or Android).
-
Clicking the notification triggers the Service Worker to open a new browser tab.
-
The tab points directly to Zitadel. Because the user is likely already authenticated in their browser, they only need to click "Approve" (or authenticate via Passkey/Biometrics if session expired).
Phase 4: Security Validation (Crucial Step)
Once the user approves the prompt, Zitadel returns an access_token and an id_token to the polling bot.
Do not blindly trust the token. Because the bot sent the push notification to a specific user, the bot must verify that the human who clicked "Approve" is the same human the bot intended to ask.
Before executing the sensitive action, the bot must:
-
Decode the
id_token. -
Extract the
sub(Subject ID) claim. -
Verify that
sub === ExpectedUserId.
If a different user intercepted the link and approved it, the system should immediately reject the token and abort the operation.