diff --git a/navi/api/routes/auth.py b/navi/api/routes/auth.py index 58e3ab4..9d1d1f6 100644 --- a/navi/api/routes/auth.py +++ b/navi/api/routes/auth.py @@ -74,7 +74,7 @@ safe_return_to = _sanitize_return_to(return_to) auth_request = client.build_authorization_request( return_to=safe_return_to, - scopes=["openid", "email", "profile", "roles", "permissions"], + scopes=["openid", "email", "profile", "roles", "permissions", "offline_access"], ) # Detect Android WebView via User-Agent (more reliable than JS navigator.userAgent) diff --git a/navi/auth/deps.py b/navi/auth/deps.py index 2223518..3fc0d36 100644 --- a/navi/auth/deps.py +++ b/navi/auth/deps.py @@ -8,6 +8,7 @@ import structlog from fastapi import Depends, HTTPException, Request +from gnexus_gauth.exceptions import TokenRefreshException from navi.config import settings @@ -153,12 +154,36 @@ token_set.expires_at or datetime.now(timezone.utc), ) log.info("auth.token_refreshed", user_id=row["user_id"]) - except Exception: - log.warning("auth.refresh_failed", session_id=session_id[:8]) - # Do NOT delete the session — transient errors (network, - # race-condition with parallel refresh) should not force - # a full re-login. - # Fallback: try API token before giving up. + except TokenRefreshException as exc: + # Refresh token is definitively invalid (expired, revoked, + # rotated by another device). Force re-login. + log.warning( + "auth.refresh_token_invalid", + session_id=session_id[:8], + user_id=row["user_id"], + reason=str(exc), + ) + # Try API token before giving up. + api_user = await _resolve_user_from_api_token(conn) + if api_user is not None: + conn.state.user = api_user + return api_user + except Exception as exc: + # Transient errors (network, timeout, 5xx from auth server). + # Do NOT delete the session — force-logout is too harsh. + # Best-effort: serve from cache if available, else try API token. + log.warning( + "auth.refresh_transient_fail", + session_id=session_id[:8], + user_id=row["user_id"], + exc_type=type(exc).__name__, + error=str(exc), + ) + cached = _get_cached_user(session_id) + if cached is not None: + log.debug("auth.fallback_to_cached_user", user_id=cached.id) + conn.state.user = cached + return cached api_user = await _resolve_user_from_api_token(conn) if api_user is not None: conn.state.user = api_user diff --git a/webclient/src/stores/auth.js b/webclient/src/stores/auth.js index 5a6a7ab..2d3e822 100644 --- a/webclient/src/stores/auth.js +++ b/webclient/src/stores/auth.js @@ -27,7 +27,8 @@ if (err.message?.includes('401')) { user.value = null } - throw err + // Swallow non-401 errors (network, 5xx) so the app stays usable + // and doesn't flash the login screen on transient failures. } finally { loading.value = false console.log('[auth] fetchMe loading=false')