Many of our customers have asked us to support easy integration of OTK with an external, existing Login-Server. Until now it was easy to connect to an external IDP but that is not always sufficient.
Well, we have listened and have updated OTK-3.5.00 to support this scenario. This blog post describes how it works.
Starting point: fresh install of OTK-3.5.00
With this version OTK introduces new API’s. In addition to /auth/oauth/v2/authorize the API’s /auth/oauth/v2/authorize/login and /auth/oauth/v2/authorize/consent have been introduced:
- …/login handles the user authentication
- …/consent handles the users consent decision (grant or deny)
As in older versions, and in compliance with RFC 6749 (OAuth 2.0) , clients will send initial authorization requests to /auth/oauth/v2/authorize. OTK runs through a validation process and either fails the request or continues.
In the case of a valid request OTK will create a session and keeps track of that. But in addition to that OTK also creates a signed JWT (JSON Web Token). That signed JWT is called sessionData and includes values associated with the initial client request. It is bound to the session that OTK keeps track of.
The response for the initial request will be a redirect to /auth/oauth/v2/authorize/login. That API receives the session identifier (sessionID) and also the session JWT (sessionData). OTK validates the JWT signature, checks if it is associated to the given sessionID and, if valid, responds with a default login page.
Once the user provided his credentials OTK validates them. If valid, OTK updates the JWT and signs it again. This time it also encrypts it due to the now sensitive content. An Auto-Form-POST to /auth/oauth/v2/authorize/consent is returned. That API displays a consent screen. It includes the client’s name and the requested SCOPE. Once the user decided to either grant or deny the request a form POST back to …/consent is executed. This time OTK takes the decision, issues an authorization_code or access_token (if granted) and redirects back to the client.
How do I leverage my Login-Server?
Well, almost very simple. But for sure … simple. There are only a few steps required. A few requirements for the external Login-Server, a few for OTK.
The external Login-Server
- MUST be able to validate and create signed & encrypted JWT using a shared secret
- MUST be able to authenticate the user
- MUST be able to redirect to /auth/oauth/v2/authorize/consent and include all required parameters
- OTK uses two shared secrets. One to generate/validate the JWT signature, one to encrypt/decrypt the JWT. These are custom values. To leverage the external Login-Server those secrets have to be configured on that server
- OTK has to be configured to redirect to the external Login-Server
If you now say: well, my Login-Server does not support JWT then this is the right opportunity to implement it. There are open source libraries available that make it very easy to do so. For an evaluation project I was able to implement JWT signing and encryption in Java within a few hours.
Sascha, I want more details!
Before you ask, here are details you need to know. They are all documented but here I will mention the most important bits and pieces.
Here is a simple but hopefully useful graphic of the possible flows:
First things first
Configure the shared secrets in OTK and your external Login-Server:
- Use the policy manager and open the policy OTK Authorization Server Configuration and set the values for “otk_session_secret” and “otk_session_secret_encryption”
- “otk_session_secret” is used to create and validate the JWT signature
- “otk_session_secret_encryption” is used to encrypt/decrypt the JWT
Configure OTK to redirect to your external Login-Server. In this example the external server will handle the login step only, not the consent decision:
- Use the policy manager and open the same policy as before. Configure the variables “host_login_server” and “path_login_server”
- “host_login_server”: it has to follow this pattern: https://your_exernal_login_server:port
- “path_login_server”: this has to be the path that is the target for the redirect, i.e.: by default: /auth/oauth/v2/authorize/login
Configure your external Login-Server to redirect to OTK after the user has been authenticated. The target is https://your_gateway:port/auth/oauth/v2/authorize/consent
TIP: When working in the policy manager ALWAYS turn on policy comments and assertion line numbers!
Requests, back and forth
In this example a client (client_id=xyz, name=ExampleApp) has been registered in OTK and is now using the authorization_code flow. The external Login-Server is hosted on https://external.domain.com/login:
OTK receives the request and validates the client_id, the redirect_uri and the SCOPE. (NOTE: in OTK only SCOPE’s that have been registered for that client can be requested).
If valid OTK responds with a redirect:
- action: “display” indicates that a login screen has to be shown. The other option would be “login” for the case of a client using “prompt=none” and “id_token_hint=an-id_token” to indicate that the user is already logged in
- sessionID: this value is opaque to the external Login-Server and has to be passed back to OTK after the user was authenticated
- sessionData: that JWT’s signature has to be validated
The JWT content is a JSON message like the example below. It has three main sections:
"scope_verified": "opened email"
"prompt": "login consent",
"scope": "openid email"
- session: the session to which the JWT is bound including the expiration time in seconds
- request_consent: the content to be displayed on the consent page (client name, SCOPE)
- request_parameters: the parameters that were received in the initial client request
Once received extract the "exp"(irationtime) from the section "session" and verify that it has not expired. Also verify that "sessionID" in the same section matches the given "sessionID" parameter. Oh, and certainly verify the JWT signature! Continue if those validations were successful.
In the simple case your server would now display an authentication page. The user authenticates (however you want him to authenticate!) and your server validates the credentials.
Now your server has to update the JSON message in the section "session". And because some of the values are sensitive (e.g.: salt, which is required to create the ‘sub’ value in the id_token) it has to be signed and encrypted. Do the following:
- current_user_consent: set it to "none"
- current_username: set it to the username
- current_user_role: set it to an appropriate value, e.g.: “admin”, “user”, “visitor” whatever is associated with that user. If no role exists set it to “user”. The roles "admin" and "user" are handled by default in OTK.
- current_user_acr: leave it untouched or set it to a value that is valid in your environment and represents the used authentication class
- current_user_authTime: set it to the time when the user authenticated. Most likely to “now” (in seconds, 10-digits)
- salt: set it to a value that is only known in your environment and associated with the authenticated user. It will be used by OTK but will never be exposed
Once this is done use the updated JSON message, sign end encrypt it. Create an Auto-Form-POST back to OTK. It has to include the following parameters:
OTK will receive the message and will do the following:
- verify that “consent” is the appropriate action
- verify that sessionID has not expired, is known and has not been used before
- decrypts sessionData and validates the signature
- verifies that sessionData is bound to the given sessionID
After that OTK will extract the given values and continues its flow. The user will be prompted with a consent page.
This blog post should enable you to get started with an integration. Always remember that it is mostly about handling the session JWT and sending it to the right location.
For questions please leave a comment.