How to integrate a login UI and authentication factors with a Connect2id server

This is a guide for integrators and frontend developers how to bind a login page (UI) as as well as one or more factors for user authentication, such as a password check, biometrics, a TPM, a USB security token, a smart card or other method to a Connect2id server.

The Connect2id server provides a web API for binding a login UI, user authentication and consent policies

The login flow and user interaction

Let's imagine we have a user, Alice, who wants to log into Wonderland App and do so with OpenID Connect. Or, that we have a plain OAuth 2.0 authorisation request by Wonderland App to access some protected resource owned by Alice.

The application, which can be web-based or mobile, sends Alice's browser to her OpenID provider / authorisation server. There she is presented with a screen to get authenticated and a second to confirm she agrees, in the case of OpenID Connect to login into Wonderland App and possibly allow access to some requested profile information. If Alice grants the request the server will send her browser back to Wonderland App with an authorisation response, continuing the flow.

An OpenID provider / authorisation server must always go through these two steps - authenticate the user, and obtain consent. However, the user interaction at each step may not be necessary:

  • When Alice gets authenticated at the Connect2id server, a session cookie can be stored in her browser, to spare her from having to enter her credentials the next time she visits. The authentication screen can be skipped then, until her session cookie expires, or the authentication level needs stepping up.

  • When Alice gives her consent to log into Wonderland App, potentially also allowing some other access, this decision of hers can be remembered, or persisted (until revoked). Subsequent login requests from the same app can then have the consent step skipped. A policy for implicit consent for selected trusted apps could also apply.

The authorisation session web API

The interaction between Alice and her OpenID provider / authorisation server happens at what the OAuth 2.0 framework calls the authorisation endpoint, because that's where she allows or denies the client access to the requested resource. In the case of OpenID Connect this resource is her identity and profile information.

The authentication and consent UIs are not hard-wired into the Connect2id server, but decoupled via a web API, the authorisation session web API.

All user interaction and low-level authentication of Alice (calling a password database, a FIDO device, etc) is thus handled outside the Connect2id server.

The benefits of the Connect2id API approach:

  • Any type of authentication factor and policy can be quickly and efficiently integrated.

  • Frontend developers can work from the comfort of their preferred language and framework, and reuse existing code and assets. Branded, personalised and otherwise tailored user experiences can be created. For example, separate logins for each type of audience (employees, consumers) and device (PC, mobile).

  • The frontend can be hosted independently from the Connect2id server, and utilise an HA proxy and CDN if needed. Updates to the frontend incur zero downtime.

  • User credentials are not disclosed to the Connect2id server, which is good for the overall security.

The interaction with the authorisation session API resembles the MVC pattern. The login handler is expected to pass the OpenID request received from the client app to the Connect2id server for processing, and when prompted by the server, interact with the end-user to perform the following:

  • (Re)authenticate the user. The Connect2id server will typically ask the handler for this if no valid session cookie was found for the user, or the client app needs the authentication level (ACR) to be stepped up. The frontend can for example handle this by rendering a screen where Alice can enter her credentials, and then checking them with a backend LDAP directory. On success the handler submits her user ID to the Connect2id server.

  • Get the end-user's consent for the requested OAuth 2.0 scope values and any OpenID Connect claims. This interaction is needed if the Connect2id server has no previous consent for Alice and the requesting client on record. The handler can obtain the consent directly from Alice, e.g. by presenting a form (explicit consent), or call upon some policy for that.

Finally, when Alice's identity and consent are established, the Connect2id generates the appropriate authorisation response, to be relayed back to the client app.

Configuring the login page URL

The login page URL must the configured as the OAuth 2.0 authorisation endpoint of the Connect2id server.

Example:

op.authz.endpoint=https://myidp.com/openid

This will enable the Connect2id server to advertise its OAuth 2.0 authorisation endpoint in its OpenID provider / authorisation server metadata, a standard JSON document where clients and developers can discover its endpoints and capabilities:

https://[base-server-url]/.well-known/openid-configuration
https://[base-server-url]/.well-known/oauth-authorization-server

General guidelines

General guidelines for the login / consent handler:

  • The login handler can be implemented with any programming language and framework. If the handler is written in Java it can be hosted inside the servlet container of the Connect2id server.

  • The login handler must be able to access the authorisation session web API, and for that it must have the configured API token, to be passed in the Authorization header of each request. The API token must be securely stored.

  • All requests to the authorisation session web API must be back-channel requestes, made from the server-side of the login handler!

  • If the login handler needs to store some context during the authorisation session, for example the current state in multi-factor authentication, it can do so in its optional data object.

  • Sometimes login handlers may need to keep some information about the user between OpenID and OAuth 2.0 requests. This information can be stored in the data field of the IdP session that the Connect2id server creates for each logged-in user.

How to use the authorisation session web API

The following diagram outlines the usage of the authorisation session API and how the Connect2id server walks the login handler through the steps of authenticating the user and obtaining her consent, if those are needed, in order to provide the final OpenID response to be send back to the client app.

Step 1. Submit OpenID request and session ID to Connect2id server

Upon receiving an OpenID authentication request the login handler must submit it to the Connect2id server, along with the session cookie (if present).

The handler must extract the URL query string and the session cookie (if one is present) from the received HTTP request. With these two parameters it then starts a new authorisation session with the Connect2id server.

Example OpenID authentication request:

https://c2id.com/login?response_type=code&client_id=123&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=openid+email&state=KEbMte3qrtNau8C7PsU1VLxd674BQfjKCARDFR1JWnE&display=popup

The extracted query string:

response_type=code&client_id=123&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=openid+email&state=KEbMte3qrtNau8C7PsU1VLxd674BQfjKCARDFR1JWnE&display=popup

Submitting the query string, together with the session cookie (if any) to the Connect2id server:

POST /authz-sessions/rest/v3/ HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/json

{
  "query"   : "response_type=code&client_id=123&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=openid+email&state=KEbMte3qrtNau8C7PsU1VLxd674BQfjKCARDFR1JWnE&display=popup",
  "sub_sid" : "WYqFXK7Q4HFnJv0hiT3Fgw.-oVkvSXgalUuMQDfEsh1lw"
}

The Connect2id server will pre-process the OpenID Connect request and proceed to the next step.

Step 2. Handle the Connect2id server response

The Connect2id server returns a JSON object indicating the next action that is required from the login handler. This depends on the value of the type member of the JSON object:

  • auth -- if type is set to this value, proceed to step 3

  • consent -- proceed to step 4

  • response -- proceed to step 5

  • error -- proceed to step 6

Step 3. Authenticate the user

The Connect2id server asks the login handler to authenticate the present user with a JSON message like this:

{
  "type"           : "auth",
  "sid"            : "WYqFXK7Q4HFnJv0hiT3Fgw.-oVkvSXgalUuMQDfEsh1lw",
  "display"        : "popup",
  "select_account" : false
}

The sid is the ID of the started authorisation session and will be needed in subsequent requests from the login handler to the API (not to be confused with the subject session ID).

The display parameter is a hint how the UI should be rendered. Look up the possible display values in the core OpenID Connect spec. Make sure the UI is responsive to support a range of screen resolutions.

The select_account parameter is optional and in most cases it won't be set. If true it indicates a request from the client app for the user to select another account for her at the IdP.

The JSON message that prompts the login page to authenticate the user is described in the authorisation session API reference.

How should the login handler proceed with authenticating the user? This is up to the IdP's policy and available infrastructure. The login page can for instance render a form for the user to input her credentials, and check them with an LDAP / Active Directory server. An additional factor, such as a hardware token, or a code sent to the user's smart phone, can also be involved.

If the Connect2id server is configured for multiple authentication levels (ACRs), clients may specify the desired level in the OpenID request. This may be requested for all logins into a given client app, or just for sensitive operations that need a higher-level of assurance (e.g. authorising a financial transaction). Either way, this will be indicated by the presence of an acr parameter in the authentication prompt.

If the user is successfully authenticated, the login handler must submit her unique user ID to the Connect2id server, using the JSON object described here:

PUT /authz-sessions/rest/v3/g6f5K6Kf6EY11zC00errCf64yLtg9lLANAcnXQk2xUE HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/json

{
  "sub" : "alice" 
}

Note this is done by an HTTP PUT to the authorisation session ID obtained above.

If the IdP supports the notion of multiple auth levels (ACRs), indicate the level at which the user was just authenticated:

PUT /authz-sessions/rest/v3/g6f5K6Kf6EY11zC00errCf64yLtg9lLANAcnXQk2xUE HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/json

{
  "sub" : "alice",
  "acr" : "http://loa.c2id.com/high"
}

At this point the login handler can also supply various settings, or override defaults, for the user session that the Connect2id server is going to create for Alice once it has received her user ID.

For example, let's make the max idle time of her browser session with the IdP one week (60 x 24 x 7 = 10080 minutes), and also save her name and email address with the session, so the login page can greet her when she comes back to the IdP for another OpenID Connect login to a client app:

PUT /authz-sessions/rest/v3/g6f5K6Kf6EY11zC00errCf64yLtg9lLANAcnXQk2xUE HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6
Content-Type: application/json

{
  "sub"      : "alice",
  "acr"      : "http://loa.c2id.com/high",
  "max_idle" : 10080,
  "data"     : { "name"  : "Alice Adams",
                 "email" : "[email protected]" }
}

Once the user authentication and session details are PUT, return to step 2.

Step 4. Obtain the user's consent

If the POST response at step 2 is a consent prompt the login handler must render a screen to get the user's confirmation to log into the client app, and if additionally requested by the client app, authorise some access represented by OAuth scope values and / or OpenID claims.

Here is an example consent prompt from the Connect2id server:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "type"        : "consent",
  "sid"         : "g6f5K6Kf6EY11zC00errCf64yLtg9lLANAcnXQk2xUE",
  "display"     : "popup",
  "sub_session" : { "sid"           : "WYqFXK7Q4HFnJv0hiT3Fgw.-oVkvSXgalUuMQDfEsh1lw",
                    "sub"           : "alice",
                    "auth_time"     : 12345678,
                    "creation_time" : 12345678,
                    "acr"           : "http://loa.c2id.com/high",
                    "max_life"      : 302400,
                    "auth_life"     : 20160,
                    "max_idle"      : 10080,
                    "data"          : { "name"  : "Alice Adams",
                                        "email" : "[email protected]" }},
  "client"      : { "client_id"        : "8cc2043",
                    "client_type"      : "confidential",
                    "application_type" : "web",
                    "name"             : "Wonderland App",
                    "uri"              : "http://wonderland.com"},
  "scope"       : { "new"       : [ "openid", "email" ],
                    "consented" : [ ] },
  "claims"      : { "new"       : { "essential" : [ "email", "email_verified" ],
                                    "voluntary" : [ ] },
                    "consented" : { "essential" : [ ],
                                    "voluntary" : [ ] } }
}

Note that we have again a sid and a display parameter included (see step 3 for their meaning).

The Connect2id server also includes the details of the session that Alice has with the IdP after her authentication (sub_session for subject session).

The login handler must take the sub_session.sid value to create / update the session cookie in the browser, so the next time Alice is sent to login she has a session established (see step 1). The sub_session.max_idle can be used to set the cookie expiration, or just make the cookie permanent to since the Connect2id server is going to manage the session expiration anyway. The sub_session.data can be used to personalise the UI.

The registered details for the client app are included (client) so the frontend can render its name, details and icon. The client details can also be used to control consent, for instance to prohibit certain clients from receiving tokens with with privileged scope.

Finally, the scope and claims parameters list the OAuth 2.0 scope values and any OpenID Connect claims that are requested by the client app. These are subdivided into scope values / claims for which the Connect2id server already has Alice's consent on record, and those which are requested for the first time. The claims have another subdivision, essential vs voluntary claims, according to OpenID Connect semantics.

Now that we've interpreted the consent prompt, how should it be handled?

This is up to chosen policy of the IdP. The consent of the requested scope and claims may be explicit (rendering a form for Alice to input her choice), implicit (subject to IdP policy), or a combination of both.

Regardless of how the consent happens, the login handler should in the end submit the consented scope values and claim names to the Connect2id server.

This is done by a simple PUT to the authorisation session, listing the authorised scope and claims:

PUT /authz-sessions/rest/v3/g6f5K6Kf6EY11zC00errCf64yLtg9lLANAcnXQk2xUE HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6

{
  "scope"  : [ "openid", "email" ],
  "claims" : [ "email", "email_verified" ]
}

Note that the authorised scope and claims can exclude some of the originally requested ones (indicating they are denied), or include additional non-requested ones.

The submitted consent supports a number of options, such as overriding individual parameters of the tokens to be issued, or implementing impersonation / on-behalf-of use cases.

If Alice chooses not to login into the client app, or deny all requested scope and claim values, the login handler must DELETE the authorisation session, which will signal the Connect2id server to generate the appropriate access_denied error response to be returned to the client app.

DELETE /authz-sessions/rest/v3/Vcg62csZGqGufdeth1eK1HlFqWKkVRmP3YBT5cTl7s0 HTTP/1.1
Host: c2id.com
Authorization: Bearer ztucZS1ZyFKgh0tUEruUtiSTXhnexmd6

With consent granted or denied, we proceed to the switch in step 2.

Step 5. Relay the final response back to the client

Arriving at this step means that the Connect2id server has generated the final OpenID authentication response, to be relayed to the client app.

If the sub_sid is set use it to create / update the session cookie in the browser. This field gets set in the final response when the user gets (re)authenticated and the consent step is skipped, due to existing consent on record for the requested scope values and / or OpenID claims.

How the response is relayed to the client depends on the mode parameter.

The login page must handle at least the query and the fragment response modes. For them just take the parameters.uri value, which contains the encoded OpenID authentication response, and redirect the browser to it.

Example final response:

HTTP/1.1 ok
Content-Type: application/json

{
  "type"       : "response",
  "mode"       : "query",
  "parameters" : { "uri" : "https://example.client.com/cb?code=5rZ_KxqZ&state=Nygv4CLQ" }
}

For a fragment response mode the JSON object will look similar, except that mode will be set to fragment.

If using an HTTP status code to perform the browser redirection, make sure it's a 303 code. Many redirection examples suggest the 302 code, however the 303 code is defined unambiguously to drop the body of an HTTP POST request. This is a security measure to prevent accidental leaking of unintended data from the IdP to the client app (e.g. if the consent form was submitted via HTTP POST, and the browser failed to convert the response to a HTTP GET for the redirect).

Example 303 redirection:

HTTP/1.1 303 See Other
Location: https://client.example.com/cb?code=637FKlXTl&state=NV2kkDp

Step 6. Display error

Client apps can also make invalid OpenID requests, and in such a case the Connect2id server will output a non-redirecting error.

Compared to other authorisation errors, such as access_denied, which are sent the client app, this type of error cannot or must not cause the user's browser to be redirected back to the client.

The login page can however safely display it to the user, so the client's developers can get notified of the issue.

HTTP/1.1 200 OK
Content-Type: application/json

{ 
  "error"             : "invalid_request",
  "error_description" : "Missing \"redirect_uri\" parameter"
}

Sample login pages

The Connect2id server comes with an example login page (coded in Java with an AngularJS front-end):

https://bitbucket.org/connect2id/connect2id-login-angularjs

An alternative login page, which exposes its entire logic and credentials in browser-side JavaScript, for exploring and playing with the authorisation session API of the Connect2id server is also available (not to be used in production, only for testing and development):

https://bitbucket.org/connect2id/connect2id-login-page-js

Tips

How to store additional state during the authorisation session?

If the login handler needs to securely store some context it can do so via the optional data JSON object of the authorisation session.

  • The current state in a multi-factor authentication procedure, for instance password and OTP based, during the authentication step.

  • Consent that needs to span multiple screens.

The data object can be set and read via its dedicated resource. In addition to that, it is available to the login handler in the authentication and consent prompts.

Important: The data resource must be set and read via back-channel calls from the login page, never via the front-end!

How to always trigger the authentication prompt?

If the login handler needs to always perform some operation during the authentication step, even when an active cookie and session are present for user, set the op.authz.alwaysPromptForAuth configuration property.

Similarly, if the handler needs to have the consent prompt to always come up, including when the user has approval on record for all requested scopes and / or claims, set the op.authz.alwaysPromptForConsent configuration property.