GuidesOAuth Apps
Implement OAuth Consent Flow
Originate OAuth flows, use hosted consent, and handle callback/token exchange correctly.
Implement OAuth Consent Flow
In the current product model, integrators usually do not render consent UI themselves. You only originate the OAuth flow and handle the callback.
The consent page route itself is platform-defined: users are redirected to the deployment frontend host at /oauth/consent.
The final callback is still sent to the OAuth client’s registered redirect_uri.
Hosted consent flow (recommended)
- OAuth client starts at
GET /oauth/authorizeon your OAuth app domain. - Runtime redirects to
GET /oauth/consent/init?handoff_id=.... - Wacht-hosted consent UI is rendered at
https://<deployment.frontend_host>/oauth/consent. - After approve/deny, backend redirects to OAuth client
redirect_uriwithcode(approve) orerror=access_denied(deny).
What you implement as an OAuth client
- Build the
/oauth/authorizeURL. - Redirect user to that URL.
- Handle callback at your registered
redirect_uri. - Exchange
codeat/oauth/token.
Where to get required values
From Console
- Open
OAuthin Console and select your app (/oauth/:slug). - Use app domain (
fqdn) to derive issuer:https://<fqdn>. - Authorization URL is
https://<fqdn>/oauth/authorize. - Token URL is
https://<fqdn>/oauth/token. - Pick
client_idfrom the app's OAuth clients table. - Use one of that client's registered
redirect_uris. - Use scopes from the app's active
supported_scopes.
From backend management API
- List app:
GET /deployments/{deployment_id}/oauth/apps - List clients for app:
GET /deployments/{deployment_id}/oauth/apps/{oauth_app_slug}/clients - Build issuer from app
fqdn:https://{fqdn} - Read
client_id,redirect_uris, andgrant_typesfrom the client object
Store in your app config
Persist these as environment/config values in your OAuth client service:
OAUTH_ISSUER(for examplehttps://auth.example.com)OAUTH_CLIENT_IDOAUTH_REDIRECT_URIOAUTH_SCOPES(space-delimited)
Example authorize redirect:
const issuer = process.env.OAUTH_ISSUER!;
const clientId = process.env.OAUTH_CLIENT_ID!;
const redirectUri = process.env.OAUTH_REDIRECT_URI!;
const scopes = process.env.OAUTH_SCOPES ?? "mcp:invoke workspace:read";
const authorizeUrl = new URL(`${issuer}/oauth/authorize`);
authorizeUrl.searchParams.set("response_type", "code");
authorizeUrl.searchParams.set("client_id", clientId);
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
authorizeUrl.searchParams.set("scope", scopes);
authorizeUrl.searchParams.set("state", crypto.randomUUID()); // Persist and validate on callback
window.location.assign(authorizeUrl.toString());Consent enforcement behavior
granted_resourcemust be a canonical Wacht URN (urn:wacht:user:*,urn:wacht:organization:*,urn:wacht:workspace:*).- Resource selection is constrained to resources the signed-in user can access.
- Approved scopes are filtered by scope definitions and the selected resource category/permission mapping.
- Consent session and CSRF tokens expire; clients should handle retry and session refresh paths.
UX requirements for production
- Show human-readable scope descriptions (
scope_definitions). - Always show target resource and app/client identity.
- Handle consent expiry (
expires_at) with retry path. - Require explicit action for destructive scopes.
Security requirements
- Use cookie session auth (
credentials: include) for consent endpoints. - Enforce CSRF token usage on submit.
- Do not auto-approve hidden scopes.
- Log user, client, scopes, resource, and decision.