Authentication levels
Every sign-in to LuxID reaches a strength level: a password alone is weaker than a password plus a second factor, which is weaker than a passkey or LuxTrust. Your Application requests a minimum level, and LuxID enforces it before the flow completes - you never receive a session weaker than you asked for. If you are new to LuxID, read Learn the basics first; this page is the reference for how levels are scored, reported in tokens, and requested.
The auth_level scale
LuxID's native authentication-strength scale is auth_level - a numeric score stored in LuxID's data model as AUTH_LEVEL. It represents the strength of the authentication factors the user presented during a given session. LuxID assigns it automatically based on which methods were completed. Partners cannot set it directly: they request a minimum via configuration or per-request parameters, and LuxID enforces it.
Levels marked not implemented are reserved in the scale but are not active in LuxID.
| Authentication method | amr values | auth_level | Status |
|---|---|---|---|
| Password only | ["pwd"] | 2 | Implemented |
| Password + OTP via SMS / voice | ["pwd", "otp"] | 3 | Implemented |
| Password + TOTP (authenticator app) | ["pwd", "totp"] | 4 | Implemented |
| (Reserved - kerberos / loginless) | - | 1 | Not implemented in LuxID |
| (Reserved - MFA fingerprint) | - | 6 | Not implemented in LuxID |
| Passkey (WebAuthn / FIDO2) | ["fido"] | 8 | Implemented |
| LuxTrust (mobile / smartcard) | ["luxtrust"] | 9 | Implemented |
Notes:
- OTP (SMS or voice) maps to
auth_level3; TOTP (authenticator app) maps toauth_level4. The two are distinct mechanisms with different delivery channels and different numeric values. - Passkey maps to 8 because a phishing-resistant hardware-bound credential is rated stronger than a shared-secret or out-of-band code.
- LuxTrust reaches 9 because it involves a qualified hardware-backed credential with associated identity proofing.
Where auth_level is used
The auth_level value plays three distinct roles inside LuxID:
- In each authentication event log - the level the user reached at sign-in is recorded against the authentication event, for audit, analytics and downstream evaluation.
- As the Application's default minimum - configured per Application with LuxID, this is the floor every sign-in to the Application must meet.
- In per-action escalation rules - an Application can attach rules that require a higher level for specific operations (for example, browsing = 2, checkout = 3, contract signing = 9). See Step-up authentication below.
auth_level in the ID Token
The auth_level numeric value surfaces in the ID Token as the luxid_auth_level claim (LuxID extension claim). The acr claim conveys LuxID's own classification of the authentication event - expressed in terms of auth_level - not an eIDAS LoA assertion. See the acr claim section below for the value format.
Example ID Token fragment:
{
"acr": "urn:luxid:acr:level:substantial",
"amr": ["pwd", "otp"],
"luxid_auth_level": 3
}
Relationship with eIDAS assurance levels (LoA)
LuxID's position
LuxID is not a notified eID scheme under eIDAS Article 9 (opens in a new tab) and does not issue eIDAS LoA assertions of its own. The acr claim in the LuxID ID Token conveys auth_level (LuxID's own classification), not a formal eIDAS LoA.
LuxID aligns with eIDAS principles structurally. LuxID's three-tier strength model mirrors the Low / Substantial / High rationale from Commission Implementing Regulation (EU) 2015/1502 (CELEX:32015R1502 (opens in a new tab)), but alignment is a design statement, not legal equivalence. Relying Parties operating under eIDAS requirements should seek their own legal assessment of whether LuxID's levels satisfy their regulatory obligations.
Informal comparison table
The table below shows the informal design alignment between auth_level values and eIDAS LoA levels. The comparison is design-aligned, not legally binding.
auth_level | Authentication method | Comparable eIDAS LoA (informal) |
|---|---|---|
| 2 | Password alone | Low |
| 3 | Password + OTP via SMS or voice | Substantial |
| 4 | Password + TOTP (authenticator app) | Substantial |
| 8 | Passkey (phishing-resistant, hardware-bound) | Substantial |
| 9 | LuxTrust (hardware-backed qualified credential) | High |
Formal eIDAS LoA assertions appear in LuxID payloads only when relayed from a qualified upstream - see the next section.
When eIDAS LoA legitimately appears in LuxID payloads
eIDAS LoA values appear in LuxID payloads exclusively as a relay from a qualified upstream source. In each case the LoA was issued by the upstream, not by LuxID.
LuxID Verified (relay from LuxTrust)
LuxTrust performs an identity verification at a declared LoA - currently Substantial. LuxID stores and relays that LoA to Partners alongside the verified attributes via the luxid_verification_level claim. The LoA value was issued by LuxTrust S.A. as a qualified trust service provider (LuxTrust S.A. (opens in a new tab)); LuxID acts as the relay.
LuxID Pro federation (relay from a corporate IdP or future notified eID)
When an upstream federated IdP provides an LoA value, LuxID interprets it via a Partner-specific regex mapping rule defined between LuxID and the Partner during integration. The mapping links accepted upstream LoA labels to an auth_level in LuxID. The upstream IdP is the source of the LoA claim; LuxID maps it to auth_level for use within the LuxID ecosystem.
Future EUDIW integration
When a European Digital Identity Wallet (EUDIW) provides a presentation, the LoA attached to the presentation originates from the EUDIW and the issuing Member State. LuxID will relay and map that LoA to auth_level using the same regex-mapping mechanism described above.
The acr claim in the ID Token
After a successful authentication, LuxID includes the acr (Authentication Context Class Reference) claim in the ID Token. This claim reports the auth_level-based classification reached during that authentication event - it may be equal to or higher than what you requested.
Example acr values (LuxID URN format):
auth_level reached | Example acr claim value |
|---|---|
| 2 (password only) | urn:luxid:acr:level:low |
| 3 or 4 (password + second factor) | urn:luxid:acr:level:substantial |
| 8 (passkey) | urn:luxid:acr:level:substantial |
| 9 (LuxTrust) | urn:luxid:acr:level:high |
Example ID Token payload fragment:
{
"iss": "https://login.luxid.lu",
"sub": "a3f8b2c1-7e4d-4a1b-9c0f-5d2e8b3a6f1c",
"aud": "your-client-id",
"exp": 1748048400,
"iat": 1748044800,
"acr": "urn:luxid:acr:level:substantial",
"amr": ["pwd", "otp"],
"luxid_auth_level": 3
}
Always verify that the acr value in the received ID Token meets your application's minimum requirement. Do not assume that because you requested a level, the level was reached. An OIDC library will return the acr claim but will not automatically enforce your application's policy - that check is your responsibility.
- Python
- Java
- PHP
- Node.js
- C#
# Example policy check
REQUIRED_ACR = "urn:luxid:acr:level:substantial"
ACR_ORDER = {
"urn:luxid:acr:level:low": 1,
"urn:luxid:acr:level:substantial": 2,
"urn:luxid:acr:level:high": 3,
}
received_acr = id_token_claims.get("acr", "")
if ACR_ORDER.get(received_acr, 0) < ACR_ORDER[REQUIRED_ACR]:
raise InsufficientAuthLevelError(received_acr)
static final String REQUIRED_ACR = "urn:luxid:acr:level:substantial";
static final Map<String, Integer> ACR_ORDER = Map.of(
"urn:luxid:acr:level:low", 1,
"urn:luxid:acr:level:substantial", 2,
"urn:luxid:acr:level:high", 3);
String receivedAcr = idTokenClaims.getStringClaim("acr");
if (ACR_ORDER.getOrDefault(receivedAcr, 0) < ACR_ORDER.get(REQUIRED_ACR)) {
throw new InsufficientAuthLevelException(receivedAcr);
}
const REQUIRED_ACR = 'urn:luxid:acr:level:substantial';
const ACR_ORDER = [
'urn:luxid:acr:level:low' => 1,
'urn:luxid:acr:level:substantial' => 2,
'urn:luxid:acr:level:high' => 3,
];
$receivedAcr = $idTokenClaims['acr'] ?? '';
if ((ACR_ORDER[$receivedAcr] ?? 0) < ACR_ORDER[REQUIRED_ACR]) {
throw new InsufficientAuthLevelException($receivedAcr);
}
const REQUIRED_ACR = 'urn:luxid:acr:level:substantial';
const ACR_ORDER = {
'urn:luxid:acr:level:low': 1,
'urn:luxid:acr:level:substantial': 2,
'urn:luxid:acr:level:high': 3,
};
const receivedAcr = idTokenClaims.acr ?? '';
if ((ACR_ORDER[receivedAcr] ?? 0) < ACR_ORDER[REQUIRED_ACR]) {
throw new InsufficientAuthLevelError(receivedAcr);
}
const string RequiredAcr = "urn:luxid:acr:level:substantial";
var acrOrder = new Dictionary<string, int>
{
["urn:luxid:acr:level:low"] = 1,
["urn:luxid:acr:level:substantial"] = 2,
["urn:luxid:acr:level:high"] = 3,
};
var receivedAcr = idTokenClaims.GetValueOrDefault("acr", "");
if (acrOrder.GetValueOrDefault(receivedAcr, 0) < acrOrder[RequiredAcr])
throw new InsufficientAuthLevelException(receivedAcr);
The amr (Authentication Methods References) claim lists the specific methods used. See Tokens and claims for the full amr value catalogue.
Requesting a minimum level
LuxID provides two complementary mechanisms for requiring a minimum level: static pre-configuration on the Application and per-request escalation parameters.
Requesting a level
The canonical request form is the acr_values authorisation-request parameter carrying a LuxID level URN:
acr_values=urn:luxid:acr:level:low
acr_values=urn:luxid:acr:level:substantial
acr_values=urn:luxid:acr:level:high
Request exactly one value per authorisation request. The set of acr_values accepted for your integration is agreed per Partner during onboarding - confirm your accepted values with LuxID before relying on them.
Static pre-configuration
Set a floor that applies to every authorisation request for an Application, without any per-request parameter:
- Application default minimum
auth_level- the floor that applies to all sign-ins to this Application. If a user's session does not meet this level, Universal Login will prompt them to complete additional factors before the flow can complete. - Per-action escalation rules - attached to the Application, these let different operations require different levels (for example, browsing = 2, checkout = 3, contract signing = 9).
Both values are set on the Application registration. See Register an Application for the field reference.
Per-request escalation via acr_values
LuxID handles whatever acr_values the Partner has agreed with LuxID during integration. LuxID uses regex matching to interpret the incoming acr_values parameter against a Partner-specific mapping table defined at integration time. The mapping links accepted values to LuxID's internal auth_level.
The canonical form is the LuxID level URN shown in Requesting a level. Alternative forms may also be agreed for a given integration:
Form 1 - LuxID level URN (canonical):
acr_values=urn%3Aluxid%3Aacr%3Alevel%3Ahigh
Form 2 - Simple numeric:
acr_values=9
Form 3 - Upstream LoA label (from a federated source that emits eIDAS-style values):
acr_values=loa3
LuxID matches the incoming value via the configured regex and maps it to an auth_level. The exact values accepted for a given Partner's integration are defined during onboarding.
Example authorisation request using the canonical LuxID level URN:
GET https://login.luxid.lu/mga/sps/oauth/oauth20/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-app.example.lu%2Fcallback
&scope=openid%20profile%20email
&acr_values=urn%3Aluxid%3Aacr%3Alevel%3Ahigh
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
Behaviour:
- If the user's current LuxID session already meets or exceeds the requested level, LuxID will not re-prompt for credentials (SSO applies, subject to session validity).
- If the user's session is at a lower level than requested, LuxID will step up authentication by prompting for an additional factor.
- If the user cannot complete the requested level (for example, no second factor enrolled and a higher level is requested), LuxID will return an
access_deniederror.
Omitting acr_values: LuxID applies the Application's configured default policy. Because this default depends entirely on your Console configuration, you should set an explicit minimum assurance level appropriate to your Application rather than relying on the unconfigured default. Check your Application settings and configure a minimum that matches the sensitivity of what the Application protects.
Per-request escalation - numeric way (luxid_min_auth_level)
LuxID also supports a proprietary extension parameter, luxid_min_auth_level, that lets you specify a minimum auth_level value numerically on the authorisation request. This is useful when you need to require a specific factor type rather than an abstracted level - for example, when your regulated context mandates LuxTrust specifically (level 9). It complements the per-Application minimum LuxID stores in its backend (see Static pre-configuration): the request parameter raises the floor for a single authorisation request above the Application's configured default.
GET https://login.luxid.lu/mga/sps/oauth/oauth20/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-app.example.lu%2Fcallback
&scope=openid%20profile%20email
&luxid_min_auth_level=9
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
When to use each approach:
| Situation | Recommended parameter |
|---|---|
| Standard access control (browsing, purchases, account changes) | acr_values - portable, standards-aligned |
| LuxTrust specifically required (bypass acr abstraction) | luxid_min_auth_level=9 |
| Analytics or debugging to understand factor adoption | Read luxid_auth_level from issued tokens |
Default to acr_values for all production use cases. The numeric luxid_min_auth_level parameter is an escape hatch for factor-method specificity; overusing it creates coupling to LuxID's internal scoring and reduces portability if the scheme is ever recalibrated.
When to require each level
The table below provides guidance on matching auth_level to application action. These are recommendations; your application's risk assessment and any applicable regulatory requirements take precedence.
| Use case | Minimum auth_level | Rationale |
|---|---|---|
| Browsing public or personalised content | 2 | Low risk; no sensitive data exposed |
| Viewing account profile, email, phone number | 2 | Read-only access to user's own data |
| Saving preferences, favourites, notification settings | 2 | Limited harm if impersonated |
| Adding items to a cart; initiating a transaction | 3 or 4 | Impersonation could cause financial harm |
| Completing a purchase or payment | 3, 4, or higher | Depends on transaction value and regulatory context |
| Changing account email address or phone number | 3 or 4 | Account takeover risk if impersonated |
| Changing password from inside an authenticated session | 3 or 4 | Credential change; high value target |
| Accessing sensitive personal data (medical, financial) | 3, 4, or higher | Depends on data sensitivity |
| Signing a contract or legal document electronically | 9 | Legal identity assertion required |
| Accessing regulated financial services | 9 | Sector-specific regulation may apply |
| Submitting a government-facing service | 9 | Legal identity required; audit trail mandatory |
| Accessing age-gated content requiring identity proof | 9 | Age claim requires identity verification |
Step-up authentication
Your application may start with a low-strength session and require a higher level only when a sensitive action is triggered - for example, initiating a payment after browsing a catalogue. This pattern is called step-up authentication.
How to implement step-up
When the user triggers a sensitive action:
- Check the
acrandauth_timeclaims in the current ID Token. - If the
acris below the required level - or if the authentication is older than your step-up freshness window - initiate a new authorisation request with the requiredacr_values. - Pass
prompt=loginif you want to force re-authentication regardless of the existing LuxID session, or omit it if you are willing to allow LuxID to step up silently (for example, by prompting for an additional factor without re-entering the password). - LuxID returns a new authorisation code. Exchange it for a new set of tokens. The new ID Token will carry the stepped-up
acrvalue. - Proceed with the sensitive action.
Example: step up to Substantial (auth_level 3 or higher):
GET https://login.luxid.lu/mga/sps/oauth/oauth20/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-app.example.lu%2Fcallback
&scope=openid
&acr_values=urn%3Aluxid%3Aacr%3Alevel%3Asubstantial
&prompt=login
&state=RANDOM_STATE_2
&code_challenge=CODE_CHALLENGE_2
&code_challenge_method=S256
Step-up freshness
Even if the user's LuxID session is at the required level, you may want to ensure the step-up happened recently (for example, within the last 5 minutes) before allowing a high-value action. Use the max_age parameter to enforce this:
&max_age=300
With max_age=300, LuxID will re-prompt for authentication if more than 300 seconds have elapsed since auth_time. The auth_time claim in the returned ID Token confirms when the authentication occurred.
Storing the step-up result
Once you have the higher-level ID Token, you may store the elevated session level in your own application session - there is no need to re-initiate the LuxID flow on every subsequent page load within the same session. Design your session state to record both the acr value and the auth_time so you can enforce freshness windows per action type.
Handling authentication failures
When you request a level the user cannot satisfy, LuxID returns an error on the redirect URI:
| Error | Likely cause |
|---|---|
access_denied | User cancelled, or cannot satisfy the requested level (for example, no second factor enrolled) |
unmet_authentication_requirements | The requested acr_values could not be met by any available method for this user |
Your application should:
- Detect the error parameter on the redirect.
- Display a helpful message - for example, directing the user to enrol a second factor at https://account.luxid.lu/ (opens in a new tab) before retrying.
- Not retry the authorisation request automatically in a loop.
Summary
auth_level | Key authentication methods | Comparable to (informal) | Typical use |
|---|---|---|---|
| 2 | Password alone | Low (informal) | Browsing, preferences, read-only profile |
| 3 | Password + OTP (SMS or voice) | Substantial (informal) | Purchases, account changes |
| 4 | Password + TOTP (authenticator app) | Substantial (informal) | Purchases, account changes |
| 8 | Passkey (WebAuthn / FIDO2) | Substantial (informal) | Phishing-resistant passwordless sign-in |
| 9 | LuxTrust (smartcard or mobile) | High (informal) | Contracts, regulated services, legal identity |
The "Comparable to" column is design-aligned, not a legal eIDAS equivalence.
Related pages
- Tokens and claims - full
acrandamrclaim reference, ID Token validation rules. - Multi-factor authentication - the MFA methods available in LuxID, per-method
auth_levelvalues, and how users enrol them. - Register an Application - configuring the default minimum
auth_leveland per-action escalation rules with LuxID. - Protect your Application - applying authentication levels as part of your security model.