Olympus Docs
InternalsHera

Hera, Hydra integration

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

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

The three challenges

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

For each, Hera 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. Hera 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 → Hera POST https://ciam/.ory/hydra/admin/oauth2/auth/requests/login/accept
      Body: { subject: "<identity.id>", remember: true }
   b. No session → Hera renders login form (Kratos flow). User logs in. Then 4a.
5. Hydra → 302 https://ciam/consent?consent_challenge=Z (consent step)
6. Hera 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. Hera 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. Hera renders confirmation (or auto-accepts if requested explicitly)
4. Hera 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

Hera'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). Hera 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

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

Custom claims in the ID token

Hera 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