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 in the direct-integration model, so you cannot rely on a shared browser session (prompt=none returns login_required) — unless the user signed in via Hosted Login, which keeps an auth-domain session where prompt=none works (see Hosted Login Guidelines). Otherwise, 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).
  • Verify the issuer, not just the audience. A client identifier is unique within a tenant, not across the platform: the same application can legitimately carry the same identifier in several tenants and in more than one installation. So a token's audience alone tells you nothing about where it came from. Check iss against the issuer you expect and verify the signature against that issuer's keys.
  • Your client identifier is not a secret — it travels in browser addresses and your users can see it. You may also choose it yourself at registration (see the API reference), which makes it guessable by design. What protects you is the registered redirect addresses, PKCE, consent, and your secret.
  • Name your tenant when you create an invitation. POST /v1/invitations is the one endpoint where you authenticate as your own application and name no tenant, so send X-Tenant-Id (your tenant slug or its UUID). Without it, an identifier shared with another tenant is refused as ambiguous rather than guessed at.
  • A deleted application's tokens stay valid until they expire. The platform refuses them, but a token you validate locally will look valid until its natural expiry — and the identifier can be registered again in the meantime. If you validate locally, treat a deletion as taking full effect after one access-token lifetime.

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.