Abstract

OAuth 2.0 workflows: Access tokens are used in token-based authentication to allow a client application to access a resource server via an API. The client application receives an access token after a user successfully authenticates with an authentication server and authorizes that client application to access a specific scope within the resource server. (Note: the EBRAINS authentication server memorizes authorizations granted by a user, so the user is only asked about granting new authorizations.) From that point on, the client application can pass the access token as a credential to the resource server when accessing it via the API. Access tokens work like a stamped ticket; the client application can continue using it as long as it remains valid, i.e. until the user explicitly logs out (thereby invalidating the token) or until the token times out.

OIDC workflows: In an OIDC workflow, the client application requests an additional openid scope from the authentication server. OIDC workflows produce both an access token and an ID token. An ID token contains ID information about the user, signed by the OIDC server.

Both access and ID tokens are represented as JSON, encoded (not encrypted) and cryptographically signed in JWT format. A nice tool to decode the contents of a JWT token is available on the JWT website.

Definitions

OAuth2

OAuth 2.0 is an authorization framework for delegated access to APIs. It involves client applications that request scopes that Resource Owners authorize/give consent to. Authorization grants are exchanged for access tokens and refresh tokens (depending on flow).

OpenId Connect

OpenID Connect or OIDC is an identity protocol that utilizes the authorization and authentication mechanisms of OAuth 2.0. While OAuth 2.0 is an authorization protocol, OIDC is an identity authentication protocol and may be used to verify the identity of a user by a client application, also called Relying Party. So, basically, OIDC is the protocol used for your authentication when you login with username/password on our EBRAINS portal. OpenId Connect is built on top of OAuth2 to add the authentication layer. The OIDC protocol provides ID Tokens which don't exist natively in OAuth2.

ID Token

ID tokens are JSON web tokens (JWTs) meant for use only by the application requesting them. For example, if there's an app that uses Google identities to log in users, the app contacts the Google authentication server, which asks the user for username/password, and then sends a signed ID token back to the app that includes information about the user. The app then verifies the validity of the token's contents and signature, and can use the information (including details like name and profile picture) to customize the user experience.

Do not use ID tokens to gain access to an API. Each token contains information for the intended audience (which is usually the recipient). According to the OpenID Connect specification, the audience of the ID token (indicated by the aud claim) must be the client ID of the application making the authentication request. If this is not the case, you should not trust the token.

Access token

Access tokens (which are sometimes also JWTs) are used to inform an API that the bearer of the token has been authorized to access the API and perform a predetermined set of actions (specified by the scopes granted).

What information can we find in a token?

By default, access and ID tokens are almost empty, containing only useful information for the authentication server. It contains a "sub" field which is a unique userId for the authentication server. You can use it as uniqueId to identify your users, or preferred_username.

The JSON snippet below shows a decoded JWT token. Many of the fields in the token can be found both in an access token and in an ID token. The token below is recognizable as an access token because its type is Bearer. As an access token, it has a scope. This token was generated by iam.ebrains.eu, (the EBRAINS authentication server) for the jupyterhub client application. Times are specified as Unix time in seconds from the epoch. Comments about each field have been added in the JSON just for this wiki.

{
 //_reserved_JWT_claims
 "jti": "f64d93e7-7ea5-4380-b755-caaacfa2df66",   // JWT ID
 "acr": "1",                                      // Authentication Context Class Reference
 "iss": "https://iam.ebrains.eu/auth/realms/hbp", // issuer
 "azp": "jupyterhub",                             // Authorized party, i.e. the client application
 "sub": "fa2db206-3eb4-403c-894a-810ebaba98e1",   // subject
 "scope": "offline_access openid",                // Scope Values
 "aud": [                                         // audience
   "xwiki",
   "team",
   "group"
  ],
 "exp": 1614788368,                               // expiration time
 "iat": 1614183568,                               // issued at time
 "auth_time": 1614183568,                         // Time when authentication occurred

 //_other_claims
 "typ": "Bearer",                                 // Bearer => this is an access token
 "session_state": "d7291a0c-d1cb-4657-857e-4cbeb4fdc6f3",
 "allowed-origins": [
   "https://lab.ebrains.eu/"
  ],
}

How can we add more information in a token?

To get more information in a token (e.g. username, email, teams, units or groups that the user belongs to), a client application should request specific scopes from the authentication server.

What are scopes?

A scope can be seen as a key to open a safe. When a client application requests a scope for a user, the authentication server will ask the user if s/he grants access to the client application to specific permissions: e.g. read access to personal information about the user (email, team, group, etc), write access to a specific resource.

All scopes available for developers and how to use them is described here.

Example: the Collaboratory Wiki client application

As a user of the Collaboratory Wiki application, each time you access that application, it authenticates you and asks for a set of scopes including team, group, clb.wiki.write, profile, email, clb.wiki.read, and openid.

If as a user, you are not yet logged in to EBRAINS in your web browser, the authentication server will take you to a login page.

If as a user, you have not yet granted those permissions/scopes to the Collaboratory Wiki application, the authentication server will take you to a page which describes the scopes and will ask for your consent to let that application have those permissions.

Screenshot 2021-02-25 at 10.49.20.png

Note the openid scope is not listed above. It is implied due to the OIDC workflow, and it grants access to an ID token.

After granting these scopes, an access token and an ID token are produced. The ID token contains the openid client scope information (email, username, full name and mitreid-sub). The access token will list the scopes that were granted (e.g. team, group). You can then pass the access token to the `/userinfo` API endpoint of the authentication server to get more information on the user's team and group as described in the Collaboratory documentation. For accessing team and group information, the resource server happens to be the same as the authentication server.

Which scope should my application request?

As per GDPR, your app should ask for as few scopes as possible; it should just ask for what it needs to get its job done. For example if it needs a user's username and email, it should just ask for the profile and email scopes.

This mechanism is similar to permissions on a smartphone. Consider a voice recording application on your Android smartphone. When you install and first run the application, it will ask you if you authorize the application to use your microphone. This is normal. But if the application is also asking to access your contacts, you should be critical and decide whether accessing your contacts is going to help the application perform the function of recording your voice.

EBRAINS might take action against application developers requesting excessive scopes.

Will my access token work in another application?

An access token authorizes a given application to access resources in 1 or more resource servers according to the scopes requested for that access token. The application, the servers and the user need to agree on the authentication server to use, in our case the EBRAINS IAM, i.e. iam.ebrains.eu. And the user needs to grant access for that application to those scopes.

Example:

When a user accesses the Collaboratory Lab, that application asks for multiple scopes including:

  • collab.drive so your Jupyter Notebook can access files in the Drive
  • clb.wiki.read and clb.wiki.write so your Jupyter Notebook can access to Collabs GET endpoints and respectively POST/PUT/DELETE endpoints
  • team and group to be authorized to access to the list of collab and groups/units a user is member of
  • profile and email to access basic informations such as username and email
  • offline_access to get a refresh token which can be used to extend the duration of the access token without intervention from the user

How apps check the scope on incoming API requests?

As a developer of a server offering an API to client applications, you get to decide what scopes you will ask for to manage permissions, and then you need to verify that each request you receive comes with the appropriate scope for the access being made.

Example: the Collaboratory Wiki application offers an API to its clients. The Collaboratory Wiki manages read and write permissions. Note: the Team application additionally manages admin permissions but the Wiki application does not use the admin permission.

In the code snippet below, the API code processes an incoming request. For every request, the code performs the following:

  1. find the access token in the authorization header of the incoming HTTP request (not shown),
  2. validate the signature of the access token (not shown),
  3. decode the access token and extract the scope (not shown), and
  4. check that the proper scope (respectively clb.wiki.read or clb.wiki.write) is included in the access token depending on what is being requested by the client.
    private void checkBearerScope(List<String> scopes, String httpMethod) throws AuthException {
       if ("GET".equals(httpMethod)) {
           if (!scopes.contains(SCOPE_CLB_READ)) {
               throw new AuthException(ACCESS_TOKEN_REQUIRE_READ_SCOPE);
           }
       } else {
           if (!(scopes.contains(SCOPE_CLB_WRITE))) {
               throw new AuthException(ACCESS_TOKEN_REQUIRE_WRITE_SCOPE);
           }
       }
   }

How to check the integrity of an access token?

Checking the scope of an access token arriving with a client request without validating the signature of the token is like buying the best lock for the front door and leaving it open.

You will find libraries to validate the signature of a token, whether in python, java or other languages. Find the library for your stack and configure it.

Some OIDC libraries might only help for the authentication of ID tokens, but will not verify an access token.

Otherwise, you will have to validate the access token manually. There are 2 ways to perform this check.

Validating the token by the OIDC server directly

You can simply send a POST request to the server. The advantage is its simplicity because you don't need any extra libraries. The inconvenient is the cost in time of an HTTP request to an external service, in this case to the EBRAINS IAM server. So if you are doing this several times per second, the Collaboratory team will at best unfriend you, no really.

The following URL explains how to check the access token that comes with an incoming API request, which they call RPT.

https://www.keycloak.org/docs/4.8/authorization_services/#_service_protection_token_introspection

So at the entry of your API. You will have to read the access token from the authorization header of the incoming request and then you will have to send the retrieved token to the introspection EBRAINS IAM endpoint.

curl -X POST \
    -H "Authorization: Basic aGVsbG8td29ybGQtYXV0aHotc2VydmljZTpzZWNyZXQ=" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d 'token_type_hint=requesting_party_token&token=eyJhbGciOiJSUz...vHEhdMY1vF8_A' \
    "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token/introspect"

The request above is using HTTP BASIC authentication to pass the client application’s credentials (client ID and secret in a Base64 encoded string) to authenticate the client attempting to inspect the token. Any other client authentication method is also supported by Keycloak.

The images below show an example of how to verify an Collaboratory Wiki token in Postman. The authorization for the introspect is also Basic Auth in this case, with the client application's ID (we called it "xwiki") and secret being entered into the UI.

Screenshot 2021-02-25 at 12.07.55.png

Screenshot 2021-02-25 at 12.08.13.png

Et voila, it works. You can see in the response of the /introspect endpoint that your token is valid and active ("active": true above).

If the token is not valid or is inactive, you will receive instead:

{
   "active": false
}

Checking the token signature is valid on your own

The second method of checking the validity of a token is to do it yourself. The advantage is it is faster and it works even if iam is offline, it will check the validity of the signature. The only inconveniant is that it cannot if an access token was manually invalidated for some reason, it can only tell you that the access token was valid at moment it was emit and for a duration defined by the expiration date.

You can check the integrity of a token directly to see if the signature match the content, for example, in java, I use the code snippet below.

You can found the public key of the IAM hbp realm here : https://iam.ebrains.eu/auth/realms/hbp

import org.keycloak.RSATokenVerifier;

AccessToken accessToken =
                    RSATokenVerifier.verifyToken(tokenString.trim(), toPublicKey(publicKey), realmUrl);

This will work even the authentication server is not reachable, e.g. because your service is offline.

Tags:
    
EBRAINS logo