OpenID Connect: authorization of internal applications from generic to standard

A few months ago, I was implementing an OpenID Connect server to control access to hundreds of our internal applications. From our own developments, convenient on a smaller scale, we moved on to the generally accepted standard. Access through a central service greatly simplifies monotonous operations, reduces the cost of implementing authorizations, allows you to find many ready-made solutions and not break your brain when developing new ones. In this article I will talk about this transition and the bumps that we managed to fill.

intro

Once upon a time ... How it all began


A few years ago, when there were too many internal applications for manual control, we wrote an application for access control inside the company. It was a simple Rails application that connected to a database with information about employees, where access to various functions was configured. Then we raised the first SSO, which was based on verification of tokens from the client and the authorization server, the token was transmitted in encrypted form with several parameters and checked on the authorization server. This was not the most convenient option, since on each internal application it was necessary to describe a considerable layer of logic, and the base of employees was completely synchronized with the authorization server.

After some time, we decided to simplify the task of centralized authorization. SSO transferred to the balancer. Using OpenResty on Lua, they added a template that checked tokens, knew which application the request was in, and could check if there was access there. This approach greatly simplified the task of controlling access of internal applications - in the code of each application, it was no longer necessary to describe additional logic. As a result, we closed the traffic externally, and the application itself did not know anything about authorization.

However, one of the problems remained unresolved. What about applications that need information about employees? You could write an API for the authorization service, but then you would have to add additional logic for each such application. In addition, we wanted to get rid of the dependence on one of our self-written applications, further oriented towards translation into OpenSource, on our internal authorization server. We will talk about him some other time. The solution to both problems was OAuth.

To generally accepted standards


OAuth is an understandable, generally accepted authorization standard, but since its functionality is not enough, OpenID Connect (OIDC) immediately began to be considered. OIDC itself is the third implementation of an open authentication standard that has spilled over into the add-in over the OAuth 2.0 protocol (open authorization protocol). This solution closes the problem of the lack of data about the end user, and also makes it possible to change the authorization provider.

However, we did not choose a specific provider and decided to add integration with OIDC for our existing authorization server. In favor of such a solution, OIDC is very flexible in terms of authorizing the end user. Thus, it was possible to implement OIDC support on your current authorization server.

image

Our way to implement our own OIDC server


1) They brought the data to the desired form


To integrate OIDC, you need to bring the current user data in a way that is understandable to the standard. In OIDC, this is called Claims. Brands are essentially the final fields in the user database (name, email, phone, etc.). There is a standard list of brands , and everything that is not included in this list is considered custom. Therefore, the first point that you need to pay attention to if you want to choose an existing OIDC provider is the ability to conveniently customize new brands.

The group of brands is combined into the next subset - Scope. During authorization, access is requested not to specific brands, namely to scopes, even if some of the hallmarks from the scope are not needed.

2) Implemented the necessary grants


The next part of OIDC integration is the selection and implementation of authorization types, the so-called grants. The further scenario of the interaction of the selected application with the authorization server will depend on the selected grant. An approximate scheme for choosing the right grant is presented in the figure below.

image

For our first application, we used the most common grant - the Authorization Code. Its difference from others is that it is three-step, i.e. passes additional verification. First, the user makes a request for authorization permission, receives a Token - Authorization Code, then with this token, as if with a ticket for travel, requests an access token. All the main interaction of this authorization scenario is based on redirects between the application and the authorization server. Read more about this grant here .

OAuth adheres to the concept that access tokens received after authorization should be temporary and change preferably on average every 10 minutes. The grant of the Authorization Code is a three-step check through redirects; to do this step every 10 minutes is, frankly, not a pleasant experience for the eyes. To solve this problem, there is another grant - Refresh Token, which we also deployed. Everything is simpler here. During the test, from another grant, in addition to the main access token, one more is issued - Refresh Token, which can be used only once and its lifetime, as a rule, is significantly longer. With this Refresh Token, when the TTL (Time to Live) of the main access token ends, a request for a new access token will come to the endpoint of another grant. The used Refresh Token is immediately reset.Such a check is two-step and can be performed in the background, invisibly to the user.

3) Customized user data output formats


After the selected grants are implemented, authorization works, it is worth mentioning the receipt of data about the end user. OIDC has a separate endpoint for this, on which you can request user data with your current access token and when relevant. And if the user data does not change so often, and you need to go after the current many times, you can come to a decision like JWT tokens. These tokens are also supported by the standard. The JWT token itself consists of three parts: header (information about the token), payload (any necessary data) and signature (signature, token is signed by the server and you can check the source of its signature in the future).

In an OIDC implementation, a JWT token is called id_token. It can be requested along with a regular access token, and all that remains is to verify the signature. The authorization server has a separate endpoint for this with a bunch of public keys in the JWK format . And speaking of this, it is worth mentioning that there is another endpoint that, based on the RFC5785 standard , reflects the current configuration of the OIDC server. It contains all the addresses of endpoints (including the address of the public keychain used for signing), supported brands and scopes, used encryption algorithms, supported grants, etc.

For example on Google:
{
 "issuer": "https://accounts.google.com",
 "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
 "device_authorization_endpoint": "https://oauth2.googleapis.com/device/code",
 "token_endpoint": "https://oauth2.googleapis.com/token",
 "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
 "revocation_endpoint": "https://oauth2.googleapis.com/revoke",
 "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
 "response_types_supported": [
  "code",
  "token",
  "id_token",
  "code token",
  "code id_token",
  "token id_token",
  "code token id_token",
  "none"
 ],
 "subject_types_supported": [
  "public"
 ],
 "id_token_signing_alg_values_supported": [
  "RS256"
 ],
 "scopes_supported": [
  "openid",
  "email",
  "profile"
 ],
 "token_endpoint_auth_methods_supported": [
  "client_secret_post",
  "client_secret_basic"
 ],
 "claims_supported": [
  "aud",
  "email",
  "email_verified",
  "exp",
  "family_name",
  "given_name",
  "iat",
  "iss",
  "locale",
  "name",
  "picture",
  "sub"
 ],
 "code_challenge_methods_supported": [
  "plain",
  "S256"
 ],
 "grant_types_supported": [
  "authorization_code",
  "refresh_token",
  "urn:ietf:params:oauth:grant-type:device_code",
  "urn:ietf:params:oauth:grant-type:jwt-bearer"
 ]
}


Thus, using id_token, you can transfer all the necessary hallmarks to the payload of the token and not contact the authorization server each time to request user information. The disadvantage of this approach is that changing user data from the server does not come immediately, but with a new access token.

Results of implementation


So, after implementing our own OIDC server and setting up connections to it on the application side, we solved the problem of transmitting user information.
Since OIDC is an open standard, we have the opportunity to choose an existing provider or server implementation. We tried Keycloak, which turned out to be very convenient to configure, after setting up and changing connection configurations on the application side, it is ready to work. On the application side, it remains only to change the connection configuration.

Speaking of existing solutions


As part of our organization, as the first OIDC server, we put together our implementation, which was supplemented as needed. After a detailed review of other ready-made solutions, we can say that this is a moot point. Concerns on the part of providers about the lack of necessary functionality served as a solution to the implementation of their server, as well as the presence of an old system in which there were various custom authorizations for some services and quite a lot of data was stored about employees. However, in ready-made implementations, there are convenience for integration. For example, Keycloak has its own user management system and data is stored directly in it, and it will not be difficult to overtake its users there. For this, Keycloak has an API that will allow you to fully implement all the necessary steps for the transfer.

Another example of a certified, interesting, in my opinion, implementation is Ory Hydra. It is interesting in that it consists of different components. For integration, you will need to link your user management service with their authorization service and expand as necessary.

Keycloak and Ory Hydra are not the only turnkey solutions. It's best to select a certified OpenID Foundation implementation. Typically, such solutions have an OpenID Certification badge.

Openid certification


Also, don't forget about existing paid providers if you don't want to keep your OIDC server. There are many good options to date.

What's next


In the near future, we are going to close the traffic to internal services in another way. We plan to transfer our current SSO on the balancer using OpenResty to a proxy based on OAuth. There are also many ready-made solutions here, for example:
github.com/bitly/oauth2_proxy
github.com/ory/oathkeeper
github.com/keycloak/keycloak-gatekeeper

Additional materials


jwt.io - a good service for checking
openid.net/developers/certified JWT tokens - a list of certified OIDC implementations

All Articles