fix(auth): silent session renewal + 401 auto-recovery
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Successful in 20s
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Has been skipped
ci / test_platform_api (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / tc_portal (push) Successful in 26s
ci / build_platform_api (push) Has been skipped
ci / build_operator (push) Successful in 31s
ci / build_portal (push) Successful in 39s
ci / deploy (push) Successful in 41s
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Successful in 20s
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Has been skipped
ci / test_platform_api (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / tc_portal (push) Successful in 26s
ci / build_platform_api (push) Has been skipped
ci / build_operator (push) Successful in 31s
ci / build_portal (push) Successful in 39s
ci / deploy (push) Successful in 41s
Idle sessions died and left a broken page: when the access token expired, nuxt-oidc-auth's automatic refresh had no refresh token to use — neither Authentik provider carried the offline_access scope mapping (and the operator never requested the scope), so the module cleared the session and every /api call 401'd until a manual F5 happened to re-auth through Authentik's still-alive SSO session. Fix 1: offline_access end to end — scope mapping attached to both live providers (and blueprints, prod + dev), operator now requests the scope. Sessions renew server-side for up to 30 days of activity (Redis store + pinned token key from earlier make the refresh tokens durable). Fix 2: client plugin in both apps — a 401 from /api sends the browser through /auth/oidc/login instead of leaving dead buttons; invisible when Authentik's session is alive, a clean sign-in screen when it isn't. Loop-guarded. Full sign-out behavior unchanged.
This commit is contained in:
@@ -76,7 +76,9 @@ export default defineNuxtConfig({
|
|||||||
logoutUrl: `${AUTH_URL}/application/o/${OPERATOR_OIDC_APP_SLUG}/end-session/`,
|
logoutUrl: `${AUTH_URL}/application/o/${OPERATOR_OIDC_APP_SLUG}/end-session/`,
|
||||||
openIdConfiguration:
|
openIdConfiguration:
|
||||||
`${AUTH_URL}/application/o/${OPERATOR_OIDC_APP_SLUG}/.well-known/openid-configuration`,
|
`${AUTH_URL}/application/o/${OPERATOR_OIDC_APP_SLUG}/.well-known/openid-configuration`,
|
||||||
scope: ['openid', 'profile', 'email', 'groups'],
|
// offline_access: refresh tokens for silent session renewal — without
|
||||||
|
// it, an expired access token kills the session (dead UI until F5).
|
||||||
|
scope: ['openid', 'profile', 'email', 'groups', 'offline_access'],
|
||||||
userNameClaim: 'preferred_username',
|
userNameClaim: 'preferred_username',
|
||||||
responseType: 'code',
|
responseType: 'code',
|
||||||
grantType: 'authorization_code',
|
grantType: 'authorization_code',
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Recover from a dead session instead of leaving a broken page: when any
|
||||||
|
// /api call returns 401, bounce the browser through the OIDC login route.
|
||||||
|
// With a live Authentik SSO session that round-trip is invisible (instant
|
||||||
|
// return to a fresh session); when Authentik's session is gone too, the
|
||||||
|
// user lands on the sign-in screen — never half-dead buttons. The
|
||||||
|
// timestamp guard prevents redirect loops if login can't restore a session.
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const KEY = 'auth-recover-at'
|
||||||
|
globalThis.$fetch = $fetch.create({
|
||||||
|
onResponseError({ request, response }) {
|
||||||
|
const url =
|
||||||
|
typeof request === 'string' ? request : request instanceof Request ? request.url : String(request)
|
||||||
|
if (response.status !== 401 || !url.startsWith('/api/')) return
|
||||||
|
const last = Number(sessionStorage.getItem(KEY) ?? 0)
|
||||||
|
if (Date.now() - last < 30_000) return
|
||||||
|
sessionStorage.setItem(KEY, String(Date.now()))
|
||||||
|
window.location.href = '/auth/oidc/login'
|
||||||
|
},
|
||||||
|
}) as typeof globalThis.$fetch
|
||||||
|
})
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Recover from a dead session instead of leaving a broken page: when any
|
||||||
|
// /api call returns 401, bounce the browser through the OIDC login route.
|
||||||
|
// With a live Authentik SSO session that round-trip is invisible (instant
|
||||||
|
// return to a fresh session); when Authentik's session is gone too, the
|
||||||
|
// user lands on the sign-in screen — never half-dead buttons. The
|
||||||
|
// timestamp guard prevents redirect loops if login can't restore a session.
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const KEY = 'auth-recover-at'
|
||||||
|
globalThis.$fetch = $fetch.create({
|
||||||
|
onResponseError({ request, response }) {
|
||||||
|
const url =
|
||||||
|
typeof request === 'string' ? request : request instanceof Request ? request.url : String(request)
|
||||||
|
if (response.status !== 401 || !url.startsWith('/api/')) return
|
||||||
|
const last = Number(sessionStorage.getItem(KEY) ?? 0)
|
||||||
|
if (Date.now() - last < 30_000) return
|
||||||
|
sessionStorage.setItem(KEY, String(Date.now()))
|
||||||
|
window.location.href = '/auth/oidc/login'
|
||||||
|
},
|
||||||
|
}) as typeof globalThis.$fetch
|
||||||
|
})
|
||||||
@@ -94,6 +94,11 @@ entries:
|
|||||||
authentik_providers_oauth2.scopemapping,
|
authentik_providers_oauth2.scopemapping,
|
||||||
[managed, "goauthentik.io/providers/oauth2/scope-profile"],
|
[managed, "goauthentik.io/providers/oauth2/scope-profile"],
|
||||||
]
|
]
|
||||||
|
# offline_access -> refresh tokens for the apps' silent session renewal.
|
||||||
|
- !Find [
|
||||||
|
authentik_providers_oauth2.scopemapping,
|
||||||
|
[managed, "goauthentik.io/providers/oauth2/scope-offline_access"],
|
||||||
|
]
|
||||||
sub_mode: hashed_user_id
|
sub_mode: hashed_user_id
|
||||||
issuer_mode: per_provider
|
issuer_mode: per_provider
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ entries:
|
|||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]]
|
||||||
|
# offline_access -> Authentik issues refresh tokens, enabling the
|
||||||
|
# apps' silent session renewal (idle sessions died without it).
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-offline_access"]]
|
||||||
sub_mode: hashed_user_id
|
sub_mode: hashed_user_id
|
||||||
issuer_mode: per_provider
|
issuer_mode: per_provider
|
||||||
# Authentik 2026.5+ enforces a per-provider grant_types allowlist; an empty
|
# Authentik 2026.5+ enforces a per-provider grant_types allowlist; an empty
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ entries:
|
|||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]]
|
||||||
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]]
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]]
|
||||||
|
# offline_access -> Authentik issues refresh tokens, enabling the
|
||||||
|
# apps' silent session renewal (idle sessions died without it).
|
||||||
|
- !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-offline_access"]]
|
||||||
sub_mode: hashed_user_id
|
sub_mode: hashed_user_id
|
||||||
issuer_mode: per_provider
|
issuer_mode: per_provider
|
||||||
# Authentik 2026.5+ enforces a per-provider grant_types allowlist; an empty
|
# Authentik 2026.5+ enforces a per-provider grant_types allowlist; an empty
|
||||||
|
|||||||
Reference in New Issue
Block a user