An OpenID Connect server is just an OAuth 2.0 server on steroids. What it does it to return the ID Token, which contains information about the authentication event for the user at the door, in addition to the Access Token. This article explains how to write a simple OpenID Connect server using an OAuth 2.0 server as the basis.
1. Add OpenID Connect request / response handling to Authorization Endpoint
Assuming you have OAuth 2.0 server code, what you need to add on top of it is code to handle a few extra parameters used for user authentication.
When a client wants to authenticate the user, it will pass the following information (the green ones are required for OAuth and the red portion is specific to OpenID Connect):
- client_id: the client id of the application;
- redirect_uri: https endpoint to which the user should be returned;
- scope: openid;
The scope can also have email, profile, address, phone, offline_access values.
- response_type: one of “id_token“, “token id_token“, “code“;
The client may also pass these.
- prompt: Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. Defined values are none, login, consent, select_account.
- max_age: Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated.
On the server side, you have to do the following upon receipt. In addition to the normal OAuth processing:
- Check if scope includes openid. If it does not, just proceed with normal OAuth processing;
- Find out what response_type the request was using;
- Create the id_token as follows:
- Make the JSON with following parameters:
- iss: https URI that indicates the issuer;
- sub: identifier of the user at the issuer;
- aud: client_id of the requesting client;
- nonce: the nonce parameter value received from the client;
- exp: expiration time of this token;
- iat: time when this token was issued;
- auth_time: time the authentication happened;
- at_hash: the first half of a hash of the access token;
- Sign it with JSON Web Signature (JWS) using your RSA private key. You should use a library for JWS. The result is the ID Token to be returned.
- If the response type is ‘code‘:
- Save the id_token and send the code back as usual in OAuth.
- Else if the response type is ‘id_token‘:
- Send the id_token back to the redirect_uri in the fragment.
- Else if the response type is ‘token id_token‘:
- Send the Access Token and ID Token back to the redirect_uri in the fragment.
- If an error occurs: You have already implemented OAuth error responses, have not you?
Unless the response type was ‘code’, you are basically done here.
2. Make Token Endpoint capable of returning ID Token
To handle a request with the response_type ‘code’, you need to add functionality to the Token Endpoint, which returns the previously created ID Token.
- When you received the ‘code’, check if it is associated with a previously created ID Token. It could be that ‘code’ has a special strtucture that indicates it or you might pull it from the database you created.
- Pull the associated ID Token out from wherever you have stored it and return it in the JSON response with the name “id_token”, e.g.:
The only addition to the normal OAuth 2.0 is this single parameter, id_token.
- refresh_token should be returned if the scope included offline_access.
- If an error occurs: this is just yet another OAuth 2.0 error response that you have already implemented.
3. (Optional) A protected resource: UserInfo Endpoint
You can actually return all the requested claims in the ID Token, but since you might want it to be small for one reason or another, you might want to return most of them from a protected resource. We have standardized resource as the UserInfo Endpoint. It is an OAuth 2.0 protected resource. Most servers will probably implement this but it is not strictly required.
The client accesses the UserInfo endpoint using the Access Token returned to it. What server needs to do is:
- Verify that the Access Token is valid;
- Pull the claims about the user associated with the Access Token out of its database;
- The claim set will be one of email, profile, address, phone or a combination of them. The scope elements above map to the following sets of claims:
- email: email and email_verified;
- profile: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
- address: address, which is a structure with the following members:
- formatted, street_address, locality, region, postal_code, country;
- phone: phone_number and phone_number_verified;
- Return them as JSON, e.g.:
"name": "Alice de Wonderland",
"family_name": "de Wonderland",
- If an error occurs, just return the OAuth 2.0 error.
There are additional features in OpenID Connect, but these are the minimal things that you need to implement as a server.
Why is there an ID Token?
As you can see, it is nearly as simple as it could be. One alternative design would be to get rid of the ID Token and create another endpoint.
We actually have started off there. We had a check_id endpoint at the beginning that returned the content of what we have as ID Token now. We moved to ID Token model later for the following reasons:
- Some social network clients could not tolerate the user experience degradation caused by the round trip time required to use the check_id endpoint;
- Some clients needed a token that can be used to create a session. The ID Token serves this purpose.
- Some clients needed to have an integrity check on access_token; we could implement it using the ID Token.
Also, we could have the returned id_token as the access token, many implementers opposed that; they wanted to keep doing what they were already doing with their access tokens.
Thus, the ID Token was the most straightforward solution. Indeed, we have made it as simple as possible, but no simpler. 😉