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.
Which information can we find in an ID 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 but which developers should not use to identity a user.
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 our available scopes for developers are described here. You will also find a full description on how to use them in the authentication process for developers.
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.
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 ID token is produced which contains the openid client scope information (email, username, full name and mitreid-sub). It will also list the other scopes that were granted in the access token that was generated (e.g. team, group). You can then pass this token to the `/userinfo` endpoint of the authentication server to get more information on the user's team and group. How to fetch this user info is described here.
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 (shouldn't this be openid?) 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.
=== CHECKED UNTIL HERE BY MARC ===
Will my access token work in another application than mine?
If the access token comes from the same authentication server (in our case, iam.ebrains.eu) and contains the scopes asked for by the other application, it will work.
Example :
XWiki Collab API is asking for clb.wiki.read and clb.wiki.write to be reach. read to GET operation, write for DELETE, PUT
Drive API is asking for collab.drive scope to access the drive API using a token.
So a token obtained after authenticating on XWiki won't be valid to access the Drive API, and vice versa, a token from the Drive authentication won't be valid to access the XWiki Collab API.
But our app Lab, when authenticating to the lab, asks for more scopes such as team , group , collab.drive , clb.wiki.write , profile , email , offline_access , clb.wiki.read
So a token obtained through the Lab authentication will contain both collab.drive and clb.wiki scope and this token from the Lab will also work on their API.
How is your app supposed to check the scope when reaching the API?
First thing to understand is that reaching the API is different than authenticating, the bearer access token means you are already authentified, so the developer will have to check that this token contains the scope that they chose as developer to access their API.
To do that there is no secret recipe ! It's development specific to your app and the technology that you use. For example on the Wiki we are checking that clb.wiki.read is present in the token and I do it manually at the entry of each endpoint.
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);
}
}
}
The list of scopes here is extracted directly from the decoded Access token provided by the user in the Authorization header of the incoming request.
But but but ... I just have to edit the JSON of the token, add the scope I want and re-encode it to hack and access the API ?
If your app doesn't check the integrity of the access token then yes, this will work, but of course, you are a good developer and you will first check that the received token is valid.
How to check the integrity of an access token ?
This is specific to the language you are using, for example python integrity checks will be different than Java integrity checks etc. Usually there is OIDC library available that will allow you to do that. Now that you understand how access token works in OIDC you should be able to find and use the OIDC library for your stack and to configure it, again no secret recipe. It should take 2-3 days to implement an integrity check.
Sometime these libs only help for the authentication but not to verify an access token in the API so you will have to do it manually
There is 2 ways to perform this check :
Validating the token by the OIDC server directly
This is the simplest way because you don't need any extra libraries, you just have to do a POST request on the server (So it will cost a request to an external service here: iam.ebrains.eu)
https://www.keycloak.org/docs/4.8/authorization_services/#_service_protection_token_introspection
They explain in this documentation how to check your access token (They call it RPT)
So at the entry of your API. You will have to read the access token from the headers of the incoming request and then you will have to send the retrieved token to this endpoint on iam
-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 and passing the client’s credentials (client ID and secret) to authenticate the client attempting to inspect the token, but you can use any other client authentication method supported by Keycloak.
Example on how to verify an XWIki token in postman, for the Authorization header it's using client secret and password but you can also use the bearer of your client if you are using your client as service account.
Et voila, it works. You can see in the response of the /introspect endpoint that your token is valid and active ! if the token is not valid or is inactive you will just receive
"active": false
}
But do I need to invoke the server every time I want to verify an access token ?
No ! See check the section below to check the signature directly
Checking that the signature is valid
You can check the integrity of a token directly to see if the signature match the content, for example, in java, I use this function
AccessToken accessToken =
RSATokenVerifier.verifyToken(tokenString.trim(), toPublicKey(publicKey), realmUrl);
This will work even offline.