In this post, I will describe the process of using the Oracle Identity Cloud Service to provide authentication for a custom web application, using the OpenID Connect protocol. I will focus on the sequence of calls between the application and IDCS in order to focus on building an understanding of how OpenID Connect actually works.
Before diving into any specifics, let's take a minute to talk about OpenID Connect and understand why we might want to use it at all. Have a read through the OpenID Connect 1.0 specification before continuing. In a nutshell, OpenID Connect (OIDC) is a "simple identity layer on top of the OAuth 2.0 protocol". While OAuth itself is often (mis)used to allow for the externalisation or delegation of authentication, it is, by design, a standard that is wholly concerned with authorisation. While it's generally true that you need to be authenticated before authorisation makes sense, there was never any formalised way to do this within OAuth itself. OIDC is the layer that adds standardised support for authentication and identity in a way that is fully compatible with and completely built on the OAuth 2.0 standard.
We're going to look at a very simple example of using OIDC to provide authentication for a custom web app. We'll be using the Authorisation Code flow here, generally a more secure flow because the user agent (i.e. the web browser) never has direct access to any of the tokens involved. The primary reasons for incorporating this functionality into an application are twofold; firstly, we may want to reduce complexity for the application developers, by removing the need to worry about authentication, password storage, user registration and the like. They can simply use an existing cloud service to handle that part for them. Secondly, and perhaps more immediately valuable, is that by doing this, we can participate in single sign-on with other applications that are also integrated with Oracle Identity Cloud Service.
Now, at this point, I do need to point out that OIDC is not Web Access Management; it does not play the same role as a WAM product like Oracle Access Manager or CA SiteMinder. There is no agent here, doing perimeter authentication and managing user sessions on behalf of your app. Your app needs to explicitly invoke an OIDC flow and explicitly handle session lifecycle on its own, dependant on the identity information that it receives back from the OpenID Connect Provider. In fact, you should probably make a point to read Chris Johnson's excellent post on why SAML is not the same as WAM, because in a lot of ways, OIDC is very similar to SAML in terms of the problems it attempts to solve. OIDC, though, is lightweight and REST/JSON-based, rather than the heavier XML-based SAML protocol.
Here's a simple list of steps explaining what our app needs to do (at run time) in order to establish a session and obtain user profile information, using the OIDC Authorization Code flow. I need to point out that you are very unlikely to ever have to implement these steps "from scratch", since there are many proven, tested OpenID Connect client libraries available for virtually any development platform or language. Treat the below as instructional, but really, don't try to roll your own in the real world.
1. When the app needs the authenticate the user, it generates a link to the OAuth2 Authorisation endpoint on the OIDC Provider (which is Oracle IDCS). This link includes the "openid" scope, the "code" response type and a local callback URL to which the Provider will redirect the browser once the authentication has been successful.
2. The user clicks the link, which results in an authentication challenge from Oracle IDCS. The user enters their credentials at the IDCS login screen and these are validated. If they are correct, an IDCS session is created for the user (represented by a browser cookie). Note that if the user already has an IDCS session due to a prior authentication, they will not be re-challenged, but will move on to the next step.
3. IDCS generates an authorisation code. This is a short, opaque string that can safely be passed as part of an HTTP payload, since it is not valuable without the corresponding Application credentials. IDCS redirects the user back to the callback URL specified in step 1, appending the code to the URL string.
4. The app extracts the authorisation code from the HTTP payload. It then makes a REST call to the OAuth2 Token endpoint on IDCS. This call is authenticated by passing the app's client ID and secret in a Basic Auth header. The body of the call includes the authorisation code.
5. Oracle IDCS authenticates the app using the client ID and secret and validates that authorisation code is valid and was issued for that app. It then returns a JSON payload containing both an Identity Token and an Access Token. Both of these tokens conform to the JSON Web Token (JWT) standard.
6. The app uses the public IDCS signing certificate to validate the Identity Token (which contains a signature). This token, once decoded, contains a number of claims that tell the app about the authentication event that took place. These include the subject, the time of authentication, the session expiry time, level of authentication and so on.
7. The app makes a REST call to the UserInfo endpoint on Oracle IDCS. This call is authenticated by passing the Access Token obtained in step 5 as a Bearer Auth header.
8. IDCS validates the provided Access Token, which is specific to the user that authenticated in step 2. The Access Token will include a number of scopes, and based on these scopes, IDCS will return the appropriate user profile information back to the app in a JSON response.
9. Now it's up to the app! It has all the identity and user profile information it needs in order to create a local user account (if this is a first-time login) and establish a local session for the user.
The first thing you need is to register your app as an OAuth Client with IDCS. This will allow you to obtain the Client ID and Secret you need. Make sure to select "Web Application" as the type.
Ensure that you select the "Authorization Code" grant type and specify a valid callback URL pointing to an endpoint on your application that can receive and process the code.
Finally, take note of your Client ID and Secret. Your app will need to store these securely in an internal credential store and use them when making calls to IDCS.
The other bit of setup that's required is for you to obtain the signing certificate that IDCS uses to sign JSON Web Tokens. Your code or JWT library will need to use this certificate to validate the signature of the Identity and Access Tokens that IDCS generates. You can obtain this certificate by issuing a GET request against the following endpoint: <IDCS_HOST>/admin/v1/SigningCert/jwk. You will need to pass an appropriately-scoped Bearer JWT in the Authorization header in order to obtain the certificate.
Now that we have the setup completed, we can look at the details. There are three main steps in the OIDC login flow and the first is to send the user to the OIDC Provider for authentication. This is done by redirecting the client browser to a particular URL on the IDCS server and passing parameters, as defined below. Note that this redirect can be accomplished in a number of different ways: the user can explicitly click a link on the app homepage to start the login flow, or the app could perhaps use an intercepting session filter to generate an automatic redirect when it detects that the browser does not have a valid application session.
In either case, the browser should be redirected to the following IDCS URL:
The following table explain the URL parameters that must be sent when constructing the authentication link:
|client_id||The Client ID for this application. IDCS uses this ID to tie the eventual authorisation code to the application that initiated the OIDC flow. No other application will be able to use the code. Note that the Client Secret is never passed in a URL string.|
|response_type||This must be specified as "code" since we're using the authorisation code flow.|
|redirect_uri||This is the URL-encoded address to which the client must be sent back once the authorisation code has been issued. Note that this must exactly match the "Redirect URL" you specified when you registered your application with IDCS. Once again, this is a safety mechanism to ensure that the code is only sent to your application endpoint, and not elsewhere.|
|scope||This must be a space-delimited strong of the required scopes. The only mandatory value here is the "openid" scope, which is required in order for IDCS to generate the ID token. You can add other scopes as well, such as "profile", "email", "phone" and "groups", depending on how much information about the user you need to retrieve later on. This point will become a bit clearer later, when we call the user info endpoint.|
|state||This is an optional parameter that you can use to maintain state within your application once the authentication redirect has taken place and protect against certain attacks.The OIDC spec defines this as an "Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie." Whatever value your application passes in here will be returned by IDCS along with the authorisation code.|
|nonce||Another optional parameter that you can use to protect against replay attacks. You should generate a strong random string value and associate it with the user session inside your application before passing it to IDCS. IDCS will include the nonce value inside the Identity Token, allowing your application to perform the necessary validation.|
Taking the above into account, we redirect the user to the following URL:
The user will need to authenticate:
And is then redirected back to the following URL:
This completes the first step in the process and we can now move on.
Now that the user has been sent back to the application with an authorisation code, we need to execute the second leg of the flow and perform a call back to the OIDC Provider to validate that code and receive the tokens we need. First of all, though, we should examine the "state" parameter value that has been sent back in the redirect to ensure that it matches the one we generated when redirecting the user. Once we've done that, our app needs to extract the "code" parameter value from the URL strong and pass this to IDCS via a back-channel REST call. I say "back channel" because this is a direct call from the application to IDCS that does not involve the user's browser in the flow. This point is key, because it ensures security of the application's client secret and also adds a layer of "hijack prevention", ensuring that any party that may intercept the browser redirect and obtain the authorisation code is not able to use it.
There are a few things we need to do here. First, we use the application's Client ID and Secret to form a basic authorisation header. We do this by concatenating them together with a colon delimiter, then Base64 encoding the whole thing. Thus 0cbf3bc1a3524d47af286f166bb03ef6:0359486e-d5ac-4339-96ca-96686a9cc223 becomes MGNiZjNiYzFhMzUyNGQ0N2FmMjg2ZjE2NmJiMDNlZjY6MDM1OTQ4NmUtZDVhYy00MzM5LTk2Y2EtOTY2ODZhOWNjMjIz and it's this value that we pass as our basic authorisation header. We POST to the IDCS token endpoint, which is at https://<IDCS_HOST>/oauth2/v1/token, passing the following values in the request body:
|grant_type||This is set to "authorization_code"|
|code||The value of the authorisation code we received.|
Here's an example, using CURL:
curl -k --header "Authorization: Basic MGNiZjNiYzFhMzUyNGQ0N2FmMjg2ZjE2NmJiMDNlZjY6MDM1OTQ4NmUtZDVhYy00MzM5LTk2Y2EtOTY2ODZhOWNjMjIz" -d "grant_type=authorization_code&code=AQIDBAUnE761LZohkZk7YCeeUHsC8m_cTK1Ck5VIkyshi7NXcAVX9thOULxfzcAzNMWLptvzo0hpVdqna4kDiZbNbdEXMTEgRU5DUllQVElPTl9LRVkxNCB7djF9NCA=" https://tenant1.idcs.internal.oracle.com:8943/oauth2/v1/token
What we get back, assuming our code is valid and has not yet expired, is a JSON response similar to the following. Note that I've shortened the token values to make the output more readable:
Note that we receive two tokens back - both of them are standard JSON Web Tokens (JWT's). The ID Token is the one that we need to use first, since it's this token that tells us about the authentication event that has taken place. We should use a JWT library within our application to validate the signature of the token we receive, using the certificate obtained earlier and then we should inspect the body of the token. I've used an online tool to parse the ID Token and this is what it contains in the payload:
"user_displayname": "Rob Otto",
Now, before we do anything at all with this token, we must check the "nonce" value and ensure that it matches the random value that we passed through to IDCS in the first step. That tells us that this is the correct ID token for the user that performed the authentication and can help mitigate any replay-type attacks.
Other than that, we have all the basic information we need in order to create a session for this user within our application. IDCS has returned the authenticated subject "rob.otto", the user's display name "Rob Otto", the opaque/non-transient user ID and also time stamps indicating when the user was authenticated and when their login session will expire. Depending on our needs, we may stop here, or, if needed, we can carry on with the third part of the flow to obtain further user profile information.
If our application requires more information and if we included additional scopes such as "profile" or "email" in our initial authentication request, we can make a further call back to IDCS to obtain this information. The key here is the other token we received from the token end point, the Access Token. Again, we can inspect this token to see that the body looks as follows:
"user_displayname": "Rob Otto",
"scope": "openid profile email",
This is a standard scoped OAuth JWT that allows us to call back to IDCS on behalf of the logged-in user. As we can see, our token includes the "profile" and "email" scopes, which will allow us to obtain some further information about our user that can be useful in building a local profile.
Our application can obtain the information it needs by making a simple GET request to the UserInfo endpoint on IDCS, which is here: https://<IDCS_HOST>/oauth2/v1/userinfo. There is no need to do anything special, other than passing the access token we obtained as a Bearer Authorisation header. Here's an example of the CURL command - again, I've shorted the access token to keep things all on one line:
curl -k --header "Authorization: Bearer eyJ4NXQjUzI1NiI6Ijg1a3E1....3Sf4u9k" https://tenant1.idcs.internal.oracle.com:8943/oauth2/v1/userinfo
The JSON object we receive back is self-explanatory and contains the necessary information about our user:
We can use this information within our application to build a local user profile and van even accomplish "just in time" provisioning of the user from IDCS if this fits our need.
This post has demonstrated, in detail, one of the simpler OpenID Connect authentication flows and has built on it further to show how user registration can be accommodated as well. There is a huge amount more than can be done using Oracle Identity Cloud Service and it's support for OAuth 2.0 and OpenID Connect. Let me know via the comments if you have any other use cases in mind that I can dive into further.
Thanks for reading and have fun diving into Oracle Identity Cloud Service.