Login loops
Users redirected back to login immediately after submitting credentials
A login loop is when the user submits valid credentials, gets redirected to the app's callback URL, and is then immediately redirected back to login, over and over.
Symptoms
- Browser address bar cycles through:
/your-app/page → /your-app/login → /hydra/oauth2/auth → /hera/login → /hydra/oauth2/auth (consent) → /your-app/callback → /your-app/page → /your-app/login → ... - No error displayed; the user just keeps seeing the login form.
- Network tab shows successful logins (Kratos returns 200) but the session doesn't persist.
Most common cause: cookie domain mismatch
The Kratos session cookie is scoped to a specific domain. If your app, Hera, and Hydra are on different domains (e.g. app.example.com, auth.example.com, kratos.internal), the cookie set on the auth.example.com domain isn't visible to app.example.com.
Check: in the browser DevTools → Application → Cookies, look at the ory_kratos_session cookie:
- What's its
Domainvalue? - What's the current page domain?
- Does the page domain match (or end with) the cookie domain?
Fix: Set Kratos's cookie domain to a parent domain that covers all subdomains:
# kratos.yml
session:
cookie:
domain: example.com # covers app.example.com and auth.example.com
path: /
same_site: LaxRestart Kratos (or trigger a config reload). Sessions are invalidated by the change.
Second most common cause: SameSite=Strict cookie
If same_site is Strict, the cookie is not sent on cross-origin redirects. Hydra's redirect to your app counts as cross-origin even if both are on the same parent domain.
Fix: Use same_site: Lax (the default for Olympus). Strict is for security-paranoid contexts where users don't follow links across domains, incompatible with OAuth2 flows.
Third cause: HTTPS / mixed protocol
Cookie set with Secure over HTTPS, won't be sent on HTTP. If your local dev redirects between HTTPS Olympus and HTTP app, the cookie disappears.
Fix: Use HTTPS everywhere in dev (Caddy gives you a self-signed cert for localhost.olympus.app, point your dev domain at this).
Fourth cause: clock skew
If the server's clock is off, the JWT iat/exp claims may be in the future or past, causing immediate rejection.
Check:
ssh prod date -u
date -u
# Difference should be < 5 secondsFix: Install ntp / systemd-timesyncd on the VPS.
Fifth cause: Hydra issuer URL mismatch
If your app validates the iss claim of the ID token against a URL, and Hydra issues with a slightly different URL (trailing slash, http vs https), the JWT verification rejects.
Check: decode your ID token; compare iss to what your app expects:
echo "$ID_TOKEN" | cut -d. -f2 | base64 -d | jq .issFix:
- Set Hydra's
urls.self.issuerto exactly what your app expects. - Verify no trailing slash mismatch.
Sixth cause: cookies blocked
Browser privacy settings (third-party cookie blocking, Firefox ETP, Safari ITP) can block cookies set on the auth domain when the user is on the app domain.
Test: open the flow in an incognito window with strict tracking protection disabled. If it works there but not in the user's primary browser, this is the cause.
Fix:
- Move auth and app to the same registered domain (so cookies are first-party).
- Or use a backend session and skip the cookie entirely (your app validates the OAuth2 access token and maintains its own session).
Diagnostic procedure
- Capture a HAR (browser DevTools → Network → Save all as HAR with content).
- Look for the
Set-Cookieheaders on the Kratos response. Note theDomainandSameSiteandSecureattributes. - Look for the request that doesn't include the cookie. Compare its domain.
- If the cookie is set on
auth.example.com(Domain=auth.example.com, no leading dot), onlyauth.example.comsees it. SetDomain=example.com(with leading dot in older RFCs) to make it visible to subdomains.
Where it isn't
- It's almost never an Olympus bug. Login loops are virtually always cookie / domain / TLS configuration.
- It's not a CSRF issue, CSRF failures return 400 with a specific error, not a redirect.