Ionize Docs
InternalsAuth

Auth, Hydra integration

How Auth handles Hydra's login/consent/logout challenges

Auth plays the login + consent UI role for Hydra. When an OAuth2 client starts an auth flow at Hydra, Hydra delegates UI to Auth; Auth returns the user identity via Hydra's admin API.

The three challenges

ChallengeWhen
login_challengeOAuth2 flow needs the user to authenticate. Hydra → Auth redirect with ?login_challenge=....
consent_challengeAfter login, the user must consent to scopes. Hydra → Auth with ?consent_challenge=....
logout_challengeAn RP-initiated logout (/oauth2/sessions/logout). Hydra → Auth with ?logout_challenge=....

For each, Auth fetches the challenge details, makes a decision (often auto), and posts back to Hydra.

Login challenge

1. App → GET https://ciam/oauth2/auth?...&client_id=X
2. Hydra → 302 https://ciam/login?login_challenge=Y
3. Auth fetches: GET https://ciam/.ory/hydra/admin/oauth2/auth/requests/login?login_challenge=Y
   Response includes: client info, subject (if Kratos session exists), requested scopes.
4. Branch:
   a. Kratos session exists → Auth POST https://ciam/.ory/hydra/admin/oauth2/auth/requests/login/accept
      Body: { subject: "<identity.id>", remember: true }
   b. No session → Auth renders login form (Kratos flow). User logs in. Then 4a.
5. Hydra → 302 https://ciam/consent?consent_challenge=Z (consent step)
6. Auth fetches: GET .../requests/consent?consent_challenge=Z
   Response: requested scopes, client metadata (including skip_consent flag).
7. Branch:
   a. metadata.skip_consent === true → auto-accept all scopes
   b. else → render consent UI
8. Auth POST .../requests/consent/accept
   Body: { grant_scope: [...], session: { id_token, access_token } }
9. Hydra issues code → 302 to app's callback

Logout challenge

1. App → GET https://ciam/oauth2/sessions/logout?id_token_hint=...
2. Hydra → 302 https://ciam/logout?logout_challenge=L
3. Auth renders confirmation (or auto-accepts if requested explicitly)
4. Auth POST .../requests/logout/accept
5. Hydra revokes session, calls Kratos to revoke its session too
6. Hydra → 302 to post_logout_redirect_uri

Talking to Hydra's admin API

Auth's server-side calls go to Hydra's admin port (:3103 / :4103):

const hydraAdmin = process.env.HYDRA_ADMIN_URL; // http://ciam-hydra:5003

await fetch(`${hydraAdmin}/admin/oauth2/auth/requests/login/accept?login_challenge=${challenge}`, {
  method: "PUT",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ subject: identityId, remember: true })
});

The admin port has no auth, security is via network ACL (admin port only reachable from inside the intranet). Auth reaching it is fine; external callers are blocked at the firewall.

const consent = await fetchConsentChallenge(challenge);
if (consent.client.metadata?.skip_consent === true) {
  await acceptConsentChallenge(challenge, {
    grant_scope: consent.requested_scope,
    session: { id_token: { /* custom claims */ } }
  });
  return Response.redirect(consent.request_url);
}
// else render the UI

Dashboard and Site clients are configured with skip_consent = true in production.

Custom claims in the ID token

Auth can shape the ID token at consent time:

await acceptConsentChallenge(challenge, {
  grant_scope: requestedScopes,
  session: {
    id_token: {
      role: identity.traits.role,
      groups: identity.traits.groups,
      // any custom claims
    }
  }
});

Hydra includes these in the issued ID token. See Cookbook, Add custom claim.

Failure modes

FailureCause
Hydra returns 404 to challenge fetchChallenge expired (1h TTL) or invalid. User must restart flow.
Accept returns 500Usually Hydra config issue, check urls.consent, urls.login are reachable from Hydra's perspective.
Consent loopSkip-consent isn't being honored. Verify metadata.skip_consent is on the right client.

On this page