Step-up authentication using protocol agnostic authenticators

As described briefly in the solution document "Single sign on for Protocol Agnostic Authenticators", 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:

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:

{
  "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:

{
  "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.