Skip to main content
Version 0.4Draft

Session management

The core problem this page solves

A recurring support issue from LuxID Partner integrations: users are prompted for their email, password, and OTP every time the application restarts - every morning when the laptop reboots, every time the mobile app is force-closed, every time the desktop application is relaunched.

This is not a LuxID policy or configuration issue. It is a consequence of the application discarding tokens on exit instead of persisting the refresh token. The fix is straightforward once the token model is understood.

This page explains:

  • What each token type does and what it does not do
  • Why discarding tokens causes repeated MFA prompts
  • How to persist sessions correctly using refresh tokens
  • How to implement silent renewal on restart
  • What to do when refresh fails
  • How to log out correctly

Responsibility boundary

Before covering tokens, a critical point about session ownership: LuxID does not manage your application's local session.

LuxID is an identity provider. It authenticates the user, issues tokens, and maintains its own server-side session for the browser-based login flow. Once tokens are issued to your application, the application is entirely responsible for:

  • Storing those tokens securely
  • Refreshing them before they expire
  • Rebuilding the local session on restart
  • Clearing them on logout
  • Revoking them when appropriate

LuxID cannot see whether your application has stored the refresh token or discarded it. It cannot tell your application that a token is about to expire. It does not push session events to your application.

caution

If your application does not persist the refresh token, every restart starts a fresh authorization request and sends the user back through LuxID. From your application's point of view this is a full login. Persist the refresh token if you want to rebuild the session silently instead of redirecting.

This does not mean the user re-enters their credentials on every restart. LuxID keeps its own server-side session, backed by a cookie on login.luxid.lu - the same single sign-on session that is shared across all LuxID partners. What happens when your application redirects depends on the state of that LuxID session:

  • LuxID session still active (the common case): LuxID recognises it and shows a short "continue with your LuxID session" confirmation. The user is signed back in with a single click - no password, no MFA. So although your application performed a full authorization request, the user experiences a one-tap return.
  • LuxID session expired: the user completes a full sign-in, including any MFA factor they have configured.

You can also force a full re-authentication regardless of the LuxID session by adding prompt=login to the authorization request - useful before a sensitive operation (changing payment details, deleting the account). See Universal Login UX - session reuse for the full behaviour and the max_age variant.


The three tokens and their roles

LuxID issues three token types in the Authorization Code flow. Each has a distinct role and lifecycle. Using a token outside its intended role causes either security problems or poor UX.

Access Token

The access token authorises API calls made on the user's behalf. Present it as a Bearer token in the Authorization header when calling LuxID's UserInfo endpoint or any other resource server that accepts LuxID user access tokens. The LuxID Partner API is different: it is a back-end API authenticated with your Partner's own credentials (a client_id and secret), not with a user's access token.

  • Lifetime: short-lived
  • Audience: the resource server (API), not your application
  • Nature: ephemeral - designed to expire quickly to limit the window of exposure if intercepted
  • Do not use for: session persistence, user identification, or any long-lived purpose

When the access token expires, do not ask the user to log in again. Obtain a new one silently using the refresh token.

ID Token

The ID token identifies the authenticated user. It is a signed JWT containing identity claims (sub, name, email, and others depending on the requested scopes). Your application uses the ID token to establish who is logged in and to build the local session.

  • Lifetime: approximately 1 hour
  • Audience: your application (the client_id in the aud claim)
  • Nature: medium-lived - suitable for building a session, but not for long-term persistence
  • Validate it: verify signature, issuer, audience, expiry, and nonce before trusting any claims

The ID token is not a mechanism for persisting the session across restarts on its own. An application that stores the ID token and uses it to bypass login on the next restart has a subtle security gap: if the user's account is deleted, their consent is revoked, or a security policy is applied at LuxID, the locally stored ID token continues to appear valid (the signature is still good, the expiry has not passed at the moment of validation) even though LuxID would reject any new token request.

Using ID-token-only persistence is acceptable only for very low-risk internal tools where this decoupling from LuxID state is explicitly accepted.

Refresh Token

The refresh token is the long-lived credential that enables silent renewal. It allows your application to obtain a new access token and a new ID token without any user interaction - no browser redirect, no credential entry, no MFA prompt.

  • Lifetime: long-lived; the exact lifetime is not returned in the token response - ask LuxID if you need the precise value (and do not hardcode it - drive renewal from the tokens' exp instead)
  • Audience: your application - it is sent only to your token endpoint call, not to APIs
  • Nature: highly sensitive - treat it with the same care as a password
  • Store it: in platform-appropriate secure storage (see §6)
  • On restart: load it from storage and exchange it for new tokens immediately

The refresh token is what makes seamless sessions possible. A user who logs in once should not need to re-authenticate for months, as long as the refresh token is valid and stored correctly.

Summary:

TokenAuthorisesDurationPersist?
Access TokenAPI callsShort-livedNo - discard on expiry
ID TokenUser identity (session bootstrap)~1 hourNo - rebuild from refresh
Refresh TokenSilent renewalLong-livedYes - store in secure storage

Persistence strategies

Approach A - ID Token only persistence

What it looks like

The application stores the ID token (or the claims extracted from it) in local storage after login. On restart, it reads those claims back and reconstructs a session without contacting LuxID.

Why it causes repeated prompts (eventually)

It does not cause repeated prompts immediately. The problem appears when:

  • The ID token expires (after ~1 hour) and there is no refresh token to renew it
  • The user manually clears application data
  • The device is rebooted and in-memory storage is cleared

Any of these events forces a full interactive login including OTP.

Security gap

The local session is disconnected from LuxID state. If the user's account is deleted, their consent is revoked, or LuxID applies a security policy (e.g. requiring re-authentication following a suspicious event), the locally stored session remains alive until it naturally expires.

When it is acceptable

Low-risk internal tools where:

  • Users are employees with a controlled device fleet
  • Account deletion or consent revocation scenarios are not expected
  • The 1-hour session lifetime is acceptable UX

Not acceptable for consumer-facing applications, applications handling personal data, or any application at Substantial or High assurance level.

What it looks like

After the initial login, the application stores the refresh token in platform-appropriate secure storage. On every subsequent restart, it loads the refresh token, calls LuxID's token endpoint with grant_type=refresh_token, receives new access and ID tokens, and rebuilds the session from the new ID token.

The user only sees a login prompt when:

  • The refresh token has expired (after a long period of not opening the application)
  • The refresh token has been revoked (explicit logout, security policy, account action)
  • The user is logging in for the first time on this device

Why this eliminates repeated OTP prompts

The OTP challenge occurs during interactive login - when LuxID is actively authenticating the user. A refresh token exchange is a silent server-to-server call. LuxID's token endpoint validates the refresh token and issues new tokens without any user-facing MFA challenge.

This is why storing and persisting the refresh token is the correct solution to the "OTP every morning" problem.

Security properties

  • The refresh token is bound to LuxID state. If the user's account is deleted or consent is revoked, the next refresh attempt will fail and the application will force an interactive login
  • The refresh token is rotated on every use. Each exchange returns a new refresh token AND immediately revokes the previous one. Always replace the stored refresh token with the new one from the response, and serialise concurrent refresh attempts - if two requests try to use the same refresh token in parallel, one succeeds and the other gets invalid_grant, forcing an interactive login
  • If the refresh token is stolen, an attacker can obtain new tokens. This makes secure storage non-negotiable
caution

Refresh-token rotation is always on. Each exchange returns a new refresh token and immediately revokes the previous one, so you must persist the new token from every response and serialise concurrent refresh attempts.


Lifecycle sequence diagrams

Initial login (authorization code + PKCE)

Application restart - silent renewal

This is the flow that eliminates the "OTP every morning" problem. No browser redirect, no user interaction, no MFA challenge.

Refresh failure - force interactive login

When the refresh token is invalid, expired, or revoked, the token endpoint returns an error. The application must catch this and send the user through the full login flow.

Causes of refresh failure include:

  • The refresh token has naturally expired
  • The user explicitly logged out from another device or from the LuxID Account portal
  • The application called the revocation endpoint
  • The user's account was deleted or suspended
  • A security policy at LuxID invalidated the session (e.g. triggered by the Security Signals feature)
  • The refresh token was used more than once (rotation is always on; reusing a spent refresh token triggers token-reuse detection)

Proactive Access Token renewal during an active session

Do not wait for an API call to fail with a 401 before refreshing the access token. Implement proactive renewal:

A threshold of 60 seconds before expiry is a reasonable default. Adjust based on your API call latency and the criticality of uninterrupted access.


Secure storage by platform

The refresh token must be stored in a location that:

  • Persists across application restarts and device reboots
  • Is inaccessible to other applications and users
  • Is protected by the platform's security mechanisms (encryption, access control)
PlatformRecommended StorageNotes
iOSKeychain (SecItemAdd / SecItemCopyMatching)Use kSecAttrAccessibleWhenUnlockedThisDeviceOnly to prevent iCloud backup and lock-screen access
AndroidAndroid Keystore + EncryptedSharedPreferencesEncryptedSharedPreferences wraps SharedPreferences with AES-256; keys are managed by Android Keystore
Web (server-rendered)Encrypted server-side session storeAccess token and refresh token stay server-side; browser receives only an httpOnly Secure session cookie
Web (SPA, pure front-end)Not recommended to store refresh tokensUse BFF pattern or prompt=none silent auth instead (see §7.2)
Desktop (Windows)Windows Credential Manager (DPAPI)PasswordVault API or DPAPI CryptProtectData
Desktop (macOS)macOS KeychainSecurity.framework - use kSecClassGenericPassword
Desktop (Linux)Secret Service API (libsecret)Backed by GNOME Keyring or KWallet depending on desktop environment
Backend serviceEncrypted secrets storeHashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or equivalent

What to never use

  • Plain localStorage or sessionStorage (web) - accessible to JavaScript, including XSS payloads
  • Plain SharedPreferences (Android) - readable on rooted devices
  • NSUserDefaults (iOS) - unencrypted, backed up to iCloud by default
  • Environment variables written to logs - environment variables themselves are acceptable for injection at runtime, but ensure they are not echoed to log output
  • Plaintext files on disk
  • The application database without additional encryption layer

Platform-specific notes

Mobile Applications

Follow RFC 8252 (opens in a new tab) (OAuth 2.0 for Native Apps):

  • Use the system browser for the authorization flow (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android) - not an embedded WebView
  • Embedded WebViews cannot benefit from the existing LuxID browser session (the user cannot be silently recognised) and have access to credentials as the user types them
  • Use a private-use URI scheme or HTTPS Universal Links / App Links for the redirect URI to prevent interception by other apps on the device

On Android, use AppAuth or a library that wraps it. On iOS, use AppAuth-iOS or ASWebAuthenticationSession directly.

SPAs without a backend-for-frontend

Pure SPAs cannot safely store refresh tokens in browser storage. The preferred session maintenance approach for SPAs is:

  1. BFF pattern (recommended): proxy all auth-related calls through a thin server-side component. The SPA never handles tokens directly
  2. Silent auth via prompt=none: when the access token expires, open a hidden iframe pointing to the LuxID authorization endpoint with prompt=none. If the LuxID browser session cookie is still valid, LuxID returns a new code immediately without user interaction. Exchange the code for new tokens. Note: this depends on the LuxID session cookie being sent in the iframe request, which is increasingly blocked by browsers as a third-party cookie
  3. Short session acceptance: accept that the SPA session will not survive a page reload or browser restart, and require login when it does not. Suitable only for applications where session persistence is not a requirement

Desktop Applications

Desktop applications are treated as public clients in most cases (the application binary can be reverse-engineered). Use PKCE without a client secret unless the application runs with a server-side component that can hold a secret securely.

The OS credential vault APIs (Windows Credential Manager, macOS Keychain, libsecret) encrypt stored credentials using the user's OS-level credentials, meaning another OS user account cannot access them.


Token lifetime reference

TokenLifetimeRenewal MechanismWhat Happens on Expiry
Access TokenShort-livedExchange refresh tokenAPI calls return 401
ID Token~1 hourExchange refresh tokenLocal session claims become stale
Refresh TokenLong-livedInteractive re-authenticationRefresh exchange fails with invalid_grant

These lifetimes reflect LuxID's current production configuration and may be adjusted by LuxID operations. You can read the access token lifetime at runtime from the expires_in field in the token response and from the exp claim in the access and ID tokens. The refresh token lifetime is not returned in the response - if you need the exact value, ask LuxID.

Drive renewal from the token, not the clock

Your application must not hardcode assumptions about token lifetimes. Implement renewal based on the exp claim in the received tokens, not on hardcoded durations.


UX vs security trade-off

StrategyUX on RestartSecurity AlignmentComplexityWhen to Use
No persistenceFull login every timeStrongestLowKiosk, shared-device, single-use sessions
ID Token onlySeamless until token expires (~1h)Weak - decoupled from LuxID stateLowLow-risk internal tools only
Refresh Token (recommended)Seamless for the refresh-token lifetimeStrong - coupled to LuxID state, with rotation and token-theft detection built inMediumAll consumer and production applications

The refresh-token strategy provides the right balance for the vast majority of LuxID Partner applications. Refresh-token rotation is always on in LuxID - you do not opt into it, and you must handle the new refresh token returned on every exchange. The only valid reason to avoid refresh tokens entirely is the absence of a safe storage mechanism on the target platform (e.g. a pure SPA without a BFF).


Logout

What clearing the local session does not do

Deleting the local session (clearing variables, deleting the session cookie, closing the session store record) removes the application's local state. It does not:

  • Invalidate the refresh token at LuxID
  • Invalidate an already-issued access token at LuxID (access tokens are short-lived bearer tokens; if you need to invalidate access immediately, discuss the available options with LuxID)
  • Terminate the user's LuxID browser session (the cookie at login.luxid.lu)

If the user expects "sign out" to mean they cannot use the refresh token to silently re-authenticate on the next restart, you must explicitly revoke the refresh token.

Revoking the Refresh Token

Call the revocation endpoint with the refresh token before clearing local state:

POST https://login.luxid.lu/mga/sps/oauth/oauth20/revoke
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>

token=<refresh_token>&token_type_hint=refresh_token

A successful revocation returns HTTP 200. After revocation, the refresh token cannot be used to obtain new tokens. The next time the user opens the application, a full interactive login is required.

See Revocation for the full API reference.

LuxID browser session

The user's LuxID browser session (the session cookie at login.luxid.lu) is separate from your application's session. Revoking the refresh token does not terminate the browser session. If the user opens another LuxID Partner application in the same browser immediately after, LuxID reuses that existing session: depending on configuration, the user is either signed in without a further prompt or simply asked to confirm they want to continue with their currently logged-in account.

info

LuxID does not expose an OIDC end_session_endpoint and does not implement RP-Initiated Logout. Omitting RP-initiated logout specifically is intentional: LuxID is the SSO authority for many Partners simultaneously, and a single application should not be able to terminate the LuxID session that other applications still rely on. Front-channel and back-channel logout are separate specifications and remain under review - see OpenID Connect - Logout.

If the user explicitly wants to end their LuxID session, redirect the browser to https://login.luxid.lu/auth/logout?client_id=<your-client-id>. LuxID then renders a logout confirmation screen that displays your application name (derived from the client_id); the user chooses whether to end the LuxID session.

Logout sequence


Common symptoms of incorrect implementation

If your users report any of the following, the root cause is almost always missing or incorrect token persistence:

SymptomLikely Cause
"Users have to log in every morning"Refresh token not persisted; discarded on application exit or OS shutdown
"The app asks for OTP every time it restarts"Same as above - refresh token not stored, so every restart triggers interactive login including MFA
"The app asks for the password again after the laptop sleeps"Access token treated as session token and not refreshed; or refresh token stored in memory only
"The session expires after a short time"Access token expiry mistaken for session expiry - the access token is short-lived and is not being renewed via the refresh token
"Users are logged out after 1 hour"ID token expiry mistaken for session expiry; ID token not being renewed via refresh token
"Token expired errors with no recovery"Expired tokens not handled; no silent renewal implemented; application shows error instead of refreshing
"Users must log in again after reinstalling the app"Refresh token stored in non-persistent storage (e.g. in-memory, or a cache cleared on reinstall) rather than Keychain/Keystore

The canonical fix for all of these is the same: store the refresh token in secure persistent storage and use it to silently obtain new tokens on restart.


Implementation checklist

Use this checklist to verify correct session management implementation:

ItemDone
Refresh token stored in platform secure storage (Keychain, Keystore, Credential Manager, etc.)
Refresh token loaded and exchanged on application startup before showing any UI
New refresh token replaces stored one after each exchange (handle token rotation)
Access token renewed proactively before expiry (not reactively after 401)
Local session rebuilt from new ID Token after each refresh exchange
invalid_grant error on refresh triggers full interactive login (not an error message)
Refresh token revoked via revocation endpoint on explicit logout
Local session cleared on logout
Tokens not written to logs, URLs, or analytics
Refresh tokens not stored in localStorage, NSUserDefaults, or plain SharedPreferences

Further reading

Updated 2026-07-03