Wiki source code of 0. How Access Tokens work

Last modified by messines on 2022/08/05 10:21

Show last authors
1 == Abstract ==
2
3 **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.
4
5 **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.
6
7 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>>https://jwt.io/]].
8
9 {{html}}
10 <iframe width="560" height="315" src="https://www.youtube.com/embed/t18YB3xDfXI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
11 {{/html}}
12
13
14 == Definitions ==
15
16 ==== OAuth2 ====
17
18 **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).
19
20 ==== OpenId Connect ====
21
22 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>>url:https://auth0.com/docs/videos/learn-identity/02-oidc-and-oauth]], 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.
23
24 ==== ID Token ====
25
26 [[ID tokens>>url:https://auth0.com/docs/tokens/id-tokens]] are [[JSON web tokens (JWTs)>>url:https://auth0.com/docs/tokens/json-web-tokens]] 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>>url:https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims]] and signature, and can use the information (including details like name and profile picture) to customize the user experience.
27
28 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.
29
30 ==== Access token ====
31
32 [[Access tokens>>url:https://auth0.com/docs/tokens/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).
33
34 == What information can we find in a token? ==
35
36 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.
37
38 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.
39
40 {{code language="json"}}
41 {
42 //_reserved_JWT_claims
43 "jti": "f64d93e7-7ea5-4380-b755-caaacfa2df66", // JWT ID
44 "acr": "1", // Authentication Context Class Reference
45 "iss": "https://iam.ebrains.eu/auth/realms/hbp", // issuer
46 "azp": "jupyterhub", // Authorized party, i.e. the client application
47 "sub": "fa2db206-3eb4-403c-894a-810ebaba98e1", // subject
48 "scope": "offline_access openid", // Scope Values
49 "aud": [ // audience
50 "xwiki",
51 "team",
52 "group"
53 ],
54 "exp": 1614788368, // expiration time
55 "iat": 1614183568, // issued at time
56 "auth_time": 1614183568, // Time when authentication occurred
57
58 //_other_claims
59 "typ": "Bearer", // Bearer => this is an access token
60 "session_state": "d7291a0c-d1cb-4657-857e-4cbeb4fdc6f3",
61 "allowed-origins": [
62 "https://lab.ebrains.eu/"
63 ],
64 }
65 {{/code}}
66
67 === How can we add more information in a token? ===
68
69 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.
70
71 === What are scopes? ===
72
73 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.
74
75 All scopes available for developers and how to use them is[[ described here>>doc:Collabs.the-collaboratory.Documentation IAM.FAQ.OIDC Clients explained.Authenticating with your OIDC client and fetching collab user info.WebHome]].
76
77 === Example: the Collaboratory Wiki client application ===
78
79 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.
80
81 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.
82
83 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.
84
85 [[image:Screenshot 2021-02-25 at 10.49.20.png||height="409" width="486"]]
86
87 Note the openid scope is not listed above. It is implied due to the OIDC workflow, and it grants access to an ID token.
88
89 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>>doc:Collabs.the-collaboratory.Documentation IAM.FAQ.OIDC Clients explained.Authenticating with your OIDC client and fetching collab user info.WebHome]]. For accessing team and group information, the resource server happens to be the same as the authentication server.
90
91 === Which scope should my application request? ===
92
93 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(% style="color:#e74c3c" %) **profile** and **email **(%%)scopes.
94
95 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.
96
97 EBRAINS might take action against application developers requesting excessive scopes.
98
99 == Will my access token work in another application? ==
100
101 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.
102
103 **Example:**
104
105 When a user accesses the **Collaboratory** **Lab, **that application asks for multiple scopes including:
106
107 * **collab.drive **so your Jupyter Notebook can access files in the Drive
108 * **clb.wiki.read** and **clb.wiki.write** so your Jupyter Notebook can** **access to Collabs **GET **endpoints** and respectively POST/PUT/DELETE **endpoints
109 * **team** and **group **to be authorized to access to the list of collab and groups/units a user is member of
110 * **profile **and **email **to access basic informations such as username and email
111 * **offline_access **to get a **refresh token** which can be used to extend the duration of the access token without intervention from the user
112
113 == How apps check the scope on incoming API requests? ==
114
115 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.
116
117 **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.
118
119 In the code snippet below, the API code processes an incoming request. For every request, the code performs the following:
120
121 1. find the **access token** in the authorization header of the incoming HTTP request (not shown),
122 1. **validate the signature** of the access token (not shown),
123 1. decode the access token and extract the scope (not shown), and
124 1. **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.
125
126 {{code language="java"}}
127 private void checkBearerScope(List<String> scopes, String httpMethod) throws AuthException {
128 if ("GET".equals(httpMethod)) {
129 if (!scopes.contains(SCOPE_CLB_READ)) {
130 throw new AuthException(ACCESS_TOKEN_REQUIRE_READ_SCOPE);
131 }
132 } else {
133 if (!(scopes.contains(SCOPE_CLB_WRITE))) {
134 throw new AuthException(ACCESS_TOKEN_REQUIRE_WRITE_SCOPE);
135 }
136 }
137 }
138 {{/code}}
139
140 == How to check the integrity of an access token? ==
141
142 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.
143
144 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.
145
146 Some OIDC libraries might only help for the authentication of ID tokens, but will not verify an access token.
147
148 Otherwise, you will have to validate the access token manually. There are 2 ways to perform this check.
149
150 === Validating the token by the OIDC server directly ===
151
152 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.
153
154 The following URL explains how to check the access token that comes with an incoming API request, which they call RPT.
155
156 [[https:~~/~~/www.keycloak.org/docs/4.8/authorization_services/#_service_protection_token_introspection>>https://www.keycloak.org/docs/4.8/authorization_services/#_service_protection_token_introspection]]
157
158 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.
159
160 {{code language="curl"}}
161 curl -X POST \
162 -H "Authorization: Basic aGVsbG8td29ybGQtYXV0aHotc2VydmljZTpzZWNyZXQ=" \
163 -H "Content-Type: application/x-www-form-urlencoded" \
164 -d 'token_type_hint=requesting_party_token&token=eyJhbGciOiJSUz...vHEhdMY1vF8_A' \
165 "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token/introspect"
166 {{/code}}
167
168 The request above is using [[HTTP BASIC authentication>>https://en.wikipedia.org/wiki/Basic_access_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.
169
170 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.
171
172 [[image:Screenshot 2021-02-25 at 12.07.55.png||height="583" width="787"]]
173
174 [[image:Screenshot 2021-02-25 at 12.08.13.png||height="249" width="790"]]
175
176 Et voila, it works. You can see in the response of the /introspect endpoint that your token is valid and active (**"active": true** above).
177
178 If the token is not valid or is inactive, you will receive instead:
179
180 {{code language="json"}}
181 {
182 "active": false
183 }
184 {{/code}}
185
186
187 === Checking the token signature is valid on your own ===
188
189 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.
190
191 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.
192
193 You can found the public key of the IAM hbp realm here : [[https:~~/~~/iam.ebrains.eu/auth/realms/hbp>>url:https://iam.ebrains.eu/auth/realms/hbp]]
194
195 {{code language="java"}}
196 import org.keycloak.RSATokenVerifier;
197
198 AccessToken accessToken =
199 RSATokenVerifier.verifyToken(tokenString.trim(), toPublicKey(publicKey), realmUrl);
200 {{/code}}
201
202 This will work even the authentication server is not reachable, e.g. because your service is offline.