Table of Contents

Step-up authentication

As described briefly in the article Single Sign On, step-up authentication is easy to configure with the single-sign-on system for protocol agnostic authenticators. You may wish to implement an authentication flow where the user can authenticate to different levels of assurance, and create additional steps in the authentication for additional assurance.

Example scenario

In this example scenario, we have the following requirements:

  • Users should be able to authenticate with assurance level: 1 to access basic services
  • Users should be able to authenticate with assurance level: 2 to access advanced services
  • SAML or OIDC Authentication Context Class Reference should be used to indicate the assurance level
  • If you already have a valid assurance level 1 session, you should only have to perform the level 2-exclusive steps for a level 2 assurance
  • If you have a valid level 2 assurance, you should still be able to access level 1 services

This can easily be solved with the following configuration:

We create a configuration for a SAML IdP and an OIDC OP that both use the same SSO group ID, and the same initial authenticator. This way, we allow access to both protocols for our authenticator configuration.

The initial authenticator should be an AgnosticDispatcher, that will look at the value "context.requestedAuthenticationContext" to determine where to send the request next. RequestedAuthenticationContext is a list of authentication context class reference values that is resolved from "acr_values" in OIDC, and "RequestedAuthenticationContext" in SAML.

In the default case, we provide an assurance level 1 log in. For assurance level 2, RequestedAuthenticationContext should contain "assuranceLevel2". Our assurance level 1 log in will be Username & Password, and our level 2 login should be Username, Password & One Touch. Note: you can of course use any protocol agnostic authenticator here; username, password & One Touch is just an example.

Below is a chart that describes how our authenticator setup will look like:

flowchart TD
    req_normal[Normal Requests]
    req_loa2[Request for assuranceLevel2]
    disp[AgnosticDispatcher]
    uidpwd["`DynamicAuthenticator
(username & password)
**SSO-enabled**`"]
    seq[SequenceAuthenticator]
    ot["One Touch for user
resolved in step 1"]
    set_loa2["DynamicAuthenticator
(headless, just to set
assuranceLevel2 values)"]
    done[Done]
    req_normal --> disp
    req_loa2 --> disp
    disp -->|Default| uidpwd
    disp -->|If assuranceLevel2| seq
    seq -->|Step 1| uidpwd
    seq -->|Step 2| ot
    seq -->|Step 3| set_loa2
    uidpwd -->|If default| done
    set_loa2 --> done
sequenceDiagram
    participant entrypoint as Entrypoint<br/>SAML IDP<br/>OIDC OP
    participant disp as AgnosticDispatcher
    participant seq as SequenceAuthenticator
    participant uidpwd as DynamicAuthenticator<br/>Username and Password<br/>SSO-enabled
    participant ot as One Touch
    participant set_loa2 as DynamicAuthenticator<br/>Set assuranceLevel2
    note over entrypoint: Normal incoming<br/>authentication request
    entrypoint->>disp: Authentication Request
    disp->>uidpwd: Authentication request<br/>Not assuranceLevel2
    uidpwd->>disp: Authenticated<br/>Might be due to SSO
    disp->>entrypoint : Authenticated
    note over entrypoint: assuranceLevel2 incoming<br/>authentication request
    entrypoint->>disp: Authentication request
    disp->>seq: Authentication request<br/>Is assuranceLevel2
    seq->>uidpwd: Authentication request
    uidpwd->>seq: Authenticated<br/>Might be due to SSO
    seq->>ot: Authentication request
    ot->>seq: Authenticated
    seq->>set_loa2: Authentication request
    note over set_loa2: Headless, just for<br/>setting assuranceLevel2<br/>values
    set_loa2->>seq: Authenticated
    seq->>disp: Authenticated
    disp->>entrypoint : Authenticated
    

Authority Configuration

SAML IDP config

{
  ...normal saml idp config,
  "authenticatorId": "dispatch",
  "allowSSO" : "true",
  "ssoGroupId": "myauthorityid"
}

OIDC OP config

{
  ...normal OIDC OP config,
  "authenticatorId": "dispatch",
  "allowSSO" : "true",
  "ssoGroupId": "myauthorityid",
  "scope_claims": [{"name": "openid", "claims": [ "acr" ] }]
}

Authenticator Configuration

AgnosticDispatcher config

{
  "id" : "ff37c25a-3e25-47d0-b3df-020af1ad10eb",
  "alias" : "dispatch",
  "name" : "AgnosticDispatcher",
  "configuration" : {
    "mapping" : [ {
      "authenticator" : "authenticatorForLevel2",
      "expression" : "context.requestedAuthenticationContext.contains('assuranceLevel2')"
    }, {
      "authenticator" : "authenticatorForLevel1",
      "expression" : "true
    } ]
  }
}

DynamicAuthenticator config (username and password)

{
  "id" : "2487e92b-9a76-478a-8337-ae93d5af4588",
  "alias" : "authenticatorForLevel1",
  "name" : "DynamicAuthenticator",
  "displayName" : "Username & Password",
  "configuration" : {
    "pipeID" : "usernamepasswordpipe",
    "setSSOParameters" : "true",
    "textEntryParameters" : [ {
      "name" : "username",
      "isUserIdentifier" : "true",
      "inputTranslationKey" : "login.messages.username"
    }, {
      "name" : "password",
      "inputTranslationKey" : "login.messages.password",
      "type" : "password"
    } ]
  }
}

AssignmentAgnostic config

{
  "id" : "2487e92b-yyyy-qqqq-8337-ae93d5af4588",
  "alias" : "assignment",
  "name" : "AssignmentAgnostic",
  "displayName" : "Assignment",
  "configuration" : {
    "usernameAttribute" : "uid"
  }
}

SequenceAuthenticator config

{
  "id" : "ff37c25t-1111-qq23-uu12-020af1ad10eb",
  "alias" : "authenticatorForLevel2",
  "name" : "SequenceAuthenticator",
  "configuration" : {
    "authenticators" : [ "authenticatorForLevel1", "assignment", "additionalPipe" ]
  }
}

DynamicAuthenticator config (LoA2)

{
  "id" : "additionalPipe",
  "name" : "DynamicAuthenticator",
  "configuration" : {
    "pipeID" : "assuranceLevel2Pipe"
  }
}

Additionally, we configure the pipes to add AuthnContextClassRef attributes to the result. For the "username & password"-pipe, we add the "acr" claim with "assuranceLevel2", and a custom AssertionProvider that uses AuthMethod "assuranceLevel1".

For the "additionalPipe" that is run for assurance level 2, we replace those attributes & that "level 1 assertion" with a new "acr" claim and an AssertionProvider that uses AuthMethod "assuranceLevel2".

Pipe configuration

For the username and password pipe:

{
  "id" : "usernamepasswordpipe",
  "description" : "Pipe performing username and password authentication",
  "name" : "Find user and validate password",
  "enabled" : "true",
  "config" : {
    "valve_refs" : "mylockoutcheckvalveid,myldapsearchvalveid,myldapbindvalveid,addClaimAssuranceLevel1,assertionProviderAssuranceLevel1"
  }
}

For the "additionalPipe":

{
  "id" : "assuranceLevel2Pipe",
  "description" : "Pipe adding assurance level 2 attributes",
  "name" : "Pipe adding assurance level 2 attributes",
  "enabled" : "true",
  "config" : {
    "valve_refs" : "addclaimAssuranceLevel2,removeAssertionLevel1,assertionProviderAssuranceLevel2"
  }
}

Valve configuration

The valves of interest (not including standard ldap search, bind, etc) are configured as follows:

{
  "id" : "addClaimAssuranceLevel1",
  "name" : "PropertyAddValve",
  "enabled" : "true",
  "config" : {
    "name" : "acr",
    "value" : "assuranceLevel1",
    "exec_if_expr": "request.contextprotocol === 'OIDC'"
  }
}
{
  "id" : "addClaimAssuranceLevel2",
  "name" : "PropertySetValve",
  "enabled" : "true",
  "config" : {
    "name" : "acr",
    "value" : "assuranceLevel2",
    "exec_if_expr": "request.contextprotocol === 'OIDC'"
  }
}
{
  "id" : "assertionProviderAssuranceLevel1",
  "name" : "AssertionProvider",
  "enabled" : "true",
  "config" : {
    "targetEntityID" : "mysamlidp",
    "nameIDAttribute" : "mynameid",
    "authMetod": "assuranceLevel1",
    "exec_if_expr": "request.contextprotocol === 'SAML'"
  }
}
{
  "id" : "assertionProviderAssuranceLevel2",
  "name" : "AssertionProvider",
  "enabled" : "true",
  "config" : {
    "targetEntityID" : "mysamlidp",
    "nameIDAttribute" : "mynameid",
    "authMetod": "assuranceLevel2"
    "exec_if_expr": "request.contextprotocol === 'SAML'"
  }
}
{
  "id" : "removeAssertionLevel1",
  "name" : "PropertyRemoveValve",
  "enabled" : "true",
  "config" : {
      "name":"SAMLResponse",
      "exec_if_expr": "request.contextprotocol === 'SAML'"
  }
}

The result

We fullfil the requirements we set up. By default, our requests will route to a username & password authentication and yield assurance level 1. If requested, we route to a username, password & onetouch authenticator and yield assurance level 2.

Our DynamicAuthenticator that handles the username & password authentication will have "setSSOParameters" enabled, which means that once you have used this for login once, you can SSO past it. This also means, you can log in with assurance level 1, and then when requesting assurance level 2 you are only prompted for OneTouch authentication, as you SSO past the first step.

This also means if you immediately request assurance level 2, you will be prompted for username & password, then OneTouch. Subsequent requests for assurance level 1 will SSO successfully.

You can also use this flow for both OpenID Connect and SAML. If you sign in with OpenID Connect for assurance level 1, you can also SSO via SAML for an assertion with assurance level 1, and vice versa. The requirements are thus fulfilled and we have set up our flow successfully.