Skip to content
Ashish.
All posts
Diagram illustrating the relationship between acr_values intent and amr fact in OpenID Connect authentication.
6 min readBackendBackend Developers#openid connect#authentication#security#oidc#identity#mfa#backend development

acr_values and amr: Requesting Authentication Strength

Explore how acr_values and amr claims define authentication strength in OpenID Connect, enabling step-up authentication and MFA enforcement.

By Ashish Kumar

In OpenID Connect (OIDC), authentication is not a binary state. You do not simply log in or you do not. You log in with a certain level of confidence that the entity on the other end is who they claim to be. This confidence is quantified by Authentication Context Class References (ACR). The mechanism for controlling this confidence lies in two specific fields: acr_values in the request and amr in the response. Misunderstanding their relationship leads to security vulnerabilities where clients assume MFA was performed when it was not.

The Request: Defining Intent with acr_values

When your application initiates an OIDC flow, it sends an authorization request to the Identity Provider (IdP). This is typically a redirect to the IdP’s /authorize endpoint. Among the parameters, you include acr_values.

acr_values is a space-separated list of strings. Each string represents an Authentication Context Class Reference. These are identifiers defined by the IdP (or standardized by standards bodies) that describe the required authentication context.

Consider a scenario where your application handles both casual browsing and financial transactions.

  1. Low Sensitivity: For a blog post, you request acr_values=urn:oid:2.16.840.1.101.3.7:2016:1 (Example IdP-specific URN). This might map to "email and password only."
  2. High Sensitivity: For a bank transfer, you request acr_values=urn:oid:2.16.840.1.101.3.7:2016:2 (Example IdP-specific URN). This maps to "email, password, AND hardware token."

Crucially, acr_values is a request. It is the client telling the IdP: "I require this level of assurance." The IdP may comply, but the client must not assume compliance until verified. If the user is already logged in with low assurance, the IdP might simply return the existing session if the policy allows, or it might challenge the user for additional factors. The mechanism here is negotiation.

A clean technical diagram showing an OIDC authorization request. The client app sends a request to the IdP /authorize endpoint. Highlight the acr_values parameter in the request URL. Use a blueprint style with clear labels and arrows indicating the flow from client to IdP. Pal…

The Response: Asserting Fact with amr

Once the user authenticates, the IdP returns an ID Token. This is a signed JWT. Inside this token, under the amr (Authentication Methods References) claim, the IdP asserts what actually happened.

amr is an array of strings. The order matters. The first element is the primary authentication method. Subsequent elements represent additional methods used to strengthen the session.

If the IdP honored your high-sensitivity request, the amr array might look like this:

{
  "amr": ["pwd", "hwk"]
}

Here, pwd indicates a password was used, and hwk (Hardware Key) indicates a second factor was presented.

If the IdP could not meet the request (e.g., the user only has a phone number for MFA, but you requested a hardware token), the amr array might contain only:

{
  "amr": ["pwd"]
}

Or, if the IdP supports multiple ways to satisfy a single acr_value, it might return:

{
  "amr": ["pwd", "sms"]
}

The Step-Up Mechanism

The most common pattern for leveraging these claims is step up authentication. This occurs when a user is already authenticated but attempts to access a resource requiring higher assurance.

Scenario: Alice’s Bank Transfer

  1. Initial Login: Alice logs into her banking app using email and password. The IdP issues an ID Token with acr_values=urn:oid:...:1 and amr=["pwd"]. The session is established.
  2. Sensitive Action: Alice clicks "Transfer $5,000." Your backend detects this requires high assurance.
  3. Step-Up Request: Your backend redirects Alice to the IdP’s /authorize endpoint again. This time, you send:
    • acr_values=urn:oid:...:2 (High Assurance)
    • prompt=none (if supported and pre-configured) or rely on the existing session.
  4. IdP Evaluation: The IdP sees the existing session has amr=["pwd"]. It compares this against the requested acr_values=urn:oid:...:2. The current assurance is insufficient.
  5. Challenge: The IdP challenges Alice. Since she doesn't have a hardware token, it falls back to SMS. Alice enters the code.
  6. New Token: The IdP issues a new ID Token. The amr claim is updated to ["pwd", "sms"]. If the IdP supports the acr claim, it may also be present to reflect the highest context achieved.
A sequence diagram illustrating step-up authentication. Show the user, the client application, and the IdP. Step 1 : User accesses sensitive resource. Step 2 : Client redirects to IdP with high acr_values. Step 3 : IdP challenges user for MFA. Step 4 : User provides MFA. Step …

Validation Logic: Trusting the Claim

The security boundary lies in your backend’s validation logic. You cannot trust the acr_values you sent; you must trust the amr claim you received.

When your backend receives the ID Token, it must perform the following check:

  1. Parse the Token: Extract the amr array.
  2. Map to Context: Translate amr values into an assurance level (e.g., pwd = Level 1, pwd+sms = Level 2).
  3. Compare: Check if the achieved assurance level meets the requirement for the resource being accessed.

If the resource requires Level 2, and the amr array contains only pwd, the request must be rejected. The client must not proceed.

Example Validation Pseudocode

def check_authentication_strength(amr_claim, required_acr):
    # Map amr values to assurance levels
    assurance_level = calculate_assurance_level(amr_claim)
    
    # Check against required level
    if assurance_level >= required_acr.level:
        return True
    else:
        return False
 
# Usage
if not check_authentication_strength(id_token.amr, required_acr="high"):
    raise PermissionError("Insufficient authentication strength")

Key Considerations

  • IdP Specificity: acr_values are often IdP-specific. While there are standard OIDs (like those from NIST or ISO), many IdPs use custom URNs. You must consult your IdP’s documentation to know which strings correspond to which mechanisms.
  • Ordering: The order of elements in the amr array is significant. The first element is the primary method. Subsequent elements are additional. Some IdPs may allow reordering, but the standard implies a chronological or primary-first sequence.
  • Security: Never rely solely on acr_values in the request. Always validate amr in the response. In backend development, validating the amr claim is critical. The amr claim is cryptographically signed by the IdP, preventing clients or third parties from altering it without detection, provided the signing key remains secret. (OIDC Core Spec)

Conclusion

acr_values and amr form a complete mechanism for authentication strength in OIDC. acr_values define the intent, allowing clients to request specific security properties. amr provides the factual record, allowing clients to verify that those properties were met. By implementing strict validation of the amr claim against the required assurance level, you enable effective step-up authentication and MFA enforcement without compromising the user experience.

This separation of intent and fact ensures that your application’s security posture is determined by the IdP’s actual authentication actions, not by your initial request.

Related posts