SSO Client Guidelines

How to act as an SSO client of the auth platform (HAP): an application that lets a user who is already signed into a sibling app (the broker) sign in to your app without re-entering credentials — by obtaining a brokered authorization code and completing a standard OIDC exchange.

This is the client half of a two-role pattern; the other half is the SSO Broker Guidelines. Read both.

Why a broker? HAP is headless and cookie-less, so you cannot rely on a shared browser session (prompt=none returns login_required). Instead, the broker app — which owns the user's session — hands you a normal OIDC authorization code. From your side it is ordinary OIDC authorization-code + PKCE; only the source of the code differs (the broker, not a HAP login page).

Build once, switch by config

Your client code is the same whether the authorization endpoint is a broker handoff URL or the platform's Hosted Login endpoint (Hosted Login Guidelines) — both speak standard OIDC Authorization-Code + PKCE. Treat the authorization endpoint as configuration (ideally read it from per-tenant discovery), and you can move between SSO-broker and Hosted Login without code changes. Write a plain, standards-compliant OIDC client; see the Client Cookbook for examples.


Prerequisites

  • Your app is registered as a HAP application in the same tenant as the broker. You have a client_id, a client_secret (register as a confidential web app), and your callback registered as a redirect URI (exact match). See the Integration Guide.
  • You know the broker's handoff URL (e.g. https://<broker-host>/sso/handoff) and the broker has allowlisted your client_id + redirect URI.
  • You know the tenant's public issuer, {HAP_BASE}/t/{tenant} — used for iss/aud/JWKS.

The flow

1. (server) Generate a PKCE pair (verifier + S256 challenge) and a random `state`. Store both in
   the user's pending-login session (server-side). KEEP THE VERIFIER SERVER-SIDE.

2. Redirect the browser to the broker's handoff URL:
     https://<broker-host>/sso/handoff
        ?client_id=<your client_id>
        &redirect_uri=<your callback, exact match>
        &code_challenge=<challenge>&code_challenge_method=S256
        &state=<state>

3. The broker (if the user has a session there) returns the browser to your callback:
     <your callback>?code=<CODE>&state=<state>
   …or, on failure: <your callback>?error=<error>&state=<state>

4. (callback, server) Verify `state` matches what you stored. Exchange the code at HAP — server to
   server, with your client_secret and the stored verifier:
     POST {HAP_BASE}/t/{tenant}/oauth2/token
       grant_type=authorization_code
       code=<CODE>
       redirect_uri=<your callback>            # must equal step 2
       code_verifier=<verifier>
       client_id=<your client_id>
       client_secret=<your client_secret>

5. Verify the returned id_token against the tenant JWKS ({HAP_BASE}/t/{tenant}/oauth2/jwks):
   check iss = the public issuer, aud = your client_id, exp, and (if you sent one) nonce.

6. Key your local user to the stable `sub` (the HAP identity id); JIT-provision on first arrival.
   Use `email`/`name` claims for display. Mint your own application session.

You now have a signed-in user with the same identity the broker authenticated.


Fallbacks

  • No broker session. If the user isn't signed into the broker, the broker runs its own login first (then resumes) or returns ?error=login_required. Handle that error by showing a "sign in" path — either send the user to the broker to log in, or run your own primary login (you are a full HAP app too; you can authenticate users directly via OTP/password per the Integration Guide).
  • Access denied / step-up. If you are a restricted app and the user isn't entitled, or a higher assurance level is required, you'll receive ?error=access_denied / interaction_required. Show the appropriate "no access" or "verify it's you" screen — these are not login errors.

Rules

  • Same tenant as the broker — cross-tenant SSO is impossible (isolated identities/keys).
  • PKCE is mandatory and yours. Generate the verifier/challenge; never send the verifier to the broker (only the challenge). This is what guarantees the brokered code is usable only by you.
  • Verify state on the callback (CSRF), and verify the id_token (iss/aud/exp/sub) against the public issuer — never trust claims unverified.
  • Exact redirect_uri everywhere (handoff, token exchange, registration) — they must all match.
  • client_secret is a secret — inject it from your platform secret store, never commit it.
  • Key on sub, not email. sub is the stable identity id; email can change (subscribe to identity.email_changed).

Lifecycle & revocation

  • Re-validate periodically with your refresh token: a refresh failure is a reliable signal the identity was suspended/deleted or access revoked — sign the user out.
  • For prompt propagation, subscribe to identity-lifecycle webhooks (identity.suspended / reinstated / deleted / email_changed) and act on them; keep refresh re-validation as the backstop.

Deploying in the same cluster as the auth service

If your app runs in the same Kubernetes cluster as HAP, use split-horizon addressing (see API Reference §1): make HAP HTTP calls (token exchange, JWKS, discovery) over the in-cluster Service (pods can't reach the cluster's own public load balancer), but keep the public issuer for iss/aud validation. The broker handoff redirect, by contrast, is a browser redirect, so it uses the broker's public URL.