Authorization Code Grant¶
The OpenID Connect authorization code grant allows clients to authenticate users and request permissions from them. User authentication is returned in the form of OpenID Connect ID tokens, and granted permissions are returned in user tokens.
The grant is started when the client constructs an authorization request URL, and directs the user agent to it. The user authenticates with Alias and selects which of the requested permissions to grant. The user is then redirected to the client's pre-registered redirect URI with an auth code in the querystring, which can be exchanged for tokens at the token endpoint.
If the user has an active session with Alias they may not be required to authenticate again. Similarly, if they have previously granted the client the requested permissions they may not be prompted again.
The openid
scope must be included in all requests as it represents permission to access the user's alias ID, and other scopes can't be used without access to that ID. It is also what indicates an OpenID Connect request, resulting in an ID token being returned along with the user token. While other scopes are optional, users must grant the openid
scope if they approve an auth request.
offline_access
is a special scope that allows the client to use granted permissions - e.g. send messages - when the user isn't logged in. It must be included in an auth request to receive a refresh token (along with other requirements), and certain client endpoints only work with users that have granted it.
Available Scopes
Scope | Required | Description |
---|---|---|
openid |
Yes | Access the user's alias ID. |
offline_access |
No | Use granted permissions when the user isn't logged in. |
contact |
No | Send messages to the user. |
nickname |
No | Access a nickname the user wishes to be known as. |
Communication channels represent permission to send a user certain types of messages, and are defined per client. This gives users more fine-grained control of the messages they want to receive, beyond the binary contact
scope. They aren't part of OAuth 2.0 or OpenID Connect specifications, but operate similarly to scopes.
A user must always grant the special _meta
channel if they grant the contact
scope. This channel is reserved for important messages about the user's connection with the client, e.g. notifying the user of a terms and conditions change, or a data breach. The client doesn't need to (and can't) include it explicitly in an auth request and it isn't explicitly listed in tokens or token endpoint responses, but it is included in a user's list of granted channels.
Authorization Request¶
GET https://projectalias.com/oauth2/authorize
Initiate an auth request by directing users to this endpoint with a number of querystring options. If the user approves the request, they will be redirect back to the client's redirect URI with an auth code, which can be exchanged at the token endpoint.
The include_granted_scopes
option allows requesting only the scopes currently required and ensuring any other previously granted scopes are included in the resulting user token. This is useful for progressively requesting scopes in response to user actions, rather than requesting all possible scopes up front. Note that the openid
scope must still always be included explicitly. The include_granted_channels
option works similarly, but for channels.
The state
option should be used to protect against CSRF attacks on the redirect URI. See the CSRF section in the OAuth 2.0 spec for details.
Similarly, the nonce
option should be used to protect against replay attacks on the redirect URI. See the nonce implementation notes in the OpenID Connect spec for details.
See the OAuth 2.0 spec for more details on the authorization endpoint.
Querystring Options
Option | Type | Required | Description |
---|---|---|---|
scope |
string |
Yes | A space separated list of scopes to request. |
include_granted_scopes |
boolean |
No | Include previously granted scopes in the request, in addition to those in scope |
channels |
string |
No | A space separated list of channels to request. Ignored if contact scope isn't requested. |
include_granted_channels |
boolean |
No | Include previously granted channels in the request, in addition to those in channels . Ignored if contact scope isn't requested. |
response_type |
string |
Yes | Must be code . |
client_id |
string |
Yes | The ID for the client. |
redirect_uri |
string |
Yes | The URI to use for the callback. Must be one of the client's pre-registered redirect URIs. |
state |
string |
No | A string selected by the client that will be included in the callback. |
nonce |
string |
No | A string selected by the client that will be included in the ID token. |
prompt |
string |
No | none , or a space separated list of prompt options from login , consent , select_account . |
max_age |
integer |
No | Maximum allowed time in seconds since the user was last authenticated with Alias. |
Callback Querystring
Field | Description |
---|---|
code |
An auth code that can be exchanged at the token endpoint. |
state |
The nonce provided by the client in the initial auth request, if any. |
Errors
If a request fails due to issues with the provided redirect_uri
or client_id
, the user will be shown an error page on Alias, and won't be redirected back to the client's redirect URI. For other client errors, or if the user rejects the request, they will be redirected back to the client's redirect URI with error information in the querystring.
See the relevant sections of the OAuth 2.0 and OpenID Connect specs for details.
Examples
Example Request
GET /oauth2/authorize?scope=openid%20offline_access%20contact%20nickname&channels=notifications%20product_updates&response_type=code&client_id=bjOB6HV7cTKtxvfr2k2ucg&redirect_uri=https%3A%2F%2Fexample.com%2Falias-callback&state=4bqlMpbTFb_yXBkHNL6R1Q&nonce=rgnxaHF9zIHaafVo4_8Pdw HTTP/1.1
Host: projectalias.com
Authorization: Bearer <access-token>
curl "https://projectalias.com/oauth2/authorize?scope=openid%20offline_access%20contact%20nickname&channels=notifications%20product_updates&response_type=code&client_id=bjOB6HV7cTKtxvfr2k2ucg&redirect_uri=https%3A%2F%2Fexample.com%2Falias-callback&state=4bqlMpbTFb_yXBkHNL6R1Q&nonce=rgnxaHF9zIHaafVo4_8Pdw" \
-H "Authorization: Bearer <access-token>"
import requests
r = requests.get('https://projectalias.com/oauth2/authorize',
headers={'Authorization': 'Bearer <access-token>'},
params={'scope': 'openid offline_access contact nickname',
'channels': 'notifications product_updated',
'response_type': 'code',
'client_id': 'bjOB6HV7cTKtxvfr2k2ucg',
'redirect_uri': 'https://example.com/alias-callback',
'state': '4bqlMpbTFb_yXBkHNL6R1Q',
'nonce': 'rgnxaHF9zIHaafVo4_8Pdw'})
Example Callback
https://example.com/alias-callback?code=8amdpfEyvNTv1JlPMGONYQ&state=4bqlMpbTFb_yXBkHNL6R1Q
Token Request¶
POST https://projectalias.com/api/oauth2/token
Exchange the auth code received at the redirect URI for tokens. Auth codes have short expiries, so this should be done as soon as possible.
ID tokens - including the nonce
- should be validated before use. See the ID token validation section in the OpenID Connect spec for details.
Tip
It's highly recommended you use a standard library to validate ID tokens. jwt.io maintains a list of JWT validation libraries.
See the OAuth 2.0 spec for more details on the token endpoint.
Authentication
Confidential clients must authenticate themselves with HTTP basic auth, using their client credentials. The client ID is used as the user-id
, and the client secret as the password
.
Warning
Confidential clients should only perform this request from a secured backend as it requires sensitive client credentials.
Request Body
The request body must be encoded as application/x-www-form-urlencoded
, and the Content-Type
header set appropriately.
Field | Required | Description |
---|---|---|
grant_type |
Yes | Must be authorization_code |
code |
Yes | An auth code obtained from the querystring of a callback to the redirect_uri . |
redirect_uri |
Yes | The redirect_uri used to obtain the auth code. |
client_id |
Maybe | The ID for the client. Required for public clients. |
Response Body
The response body is encoded as application/json
.
Field | Type | Description |
---|---|---|
access_token |
string |
A client token. |
token_type |
string |
Always Bearer . |
expires_in |
integer |
How many seconds the token will be valid for. Tokens are valid for 30 minutes. |
scope |
string |
A space separated list of scopes the token has. |
channels |
string |
A space separated list of channels the token has. Only included if the token has the contact scope. |
id_token |
string |
An ID client token for the user. |
refresh_token |
string |
A refresh token. Only included in certain situations. |
Errors
In case of an error, the endpoint will endevour to respond with an appropriate HTTP status code. It may include details of the error in an application/json
response body.
Field | Type | Description |
---|---|---|
error |
string |
An error code. |
error_description |
string |
An optional human-readable description of the error. |
See the relevant section of the OAuth 2.0 spec for details of the OAuth 2.0 specific error codes. Note that you may receive error codes not defined in the spec.
Examples
Example Request
POST /api/oauth2/token HTTP/1.1
Host: projectalias.com
Authorization: Basic <basic-credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=8amdpfEyvNTv1JlPMGONYQ&redirect_uri=https%3A%2F%2Fexample.com%2Falias-callback
curl -XPOST "https://projectalias.com/api/oauth2/token" \
-H "Authorization: Basic <basic-credentials>" \
-d "grant_type=authorization_code" \
-d "code=8amdpfEyvNTv1JlPMGONYQ" \
-d "redirect_uri=https://example.com/alias-callback"
import requests
r = requests.post('https://projectalias.com/api/oauth2/token',
headers={'Authorization': 'Basic <basic-credentials>'}
data={'grant_type': 'authorization_code',
'code': '8amdpfEyvNTv1JlPMGONYQ',
'redirect_uri': 'https://example.com/alias-callback'})
Example Response
{
"access_token": "x-o7XwWiGWCBkH_TKV-slC8IP_DACpN2k9qQO3q2sV1y4b_fvJgBBIP7xlnmpW1ZZ2JojcpK-SP8G1dFKETn7zBdr8Q_xQsbGQ3upIY45u1AL8vhQLsMWOK8Npv4KlblD_6Ium0jm-16cYBSd8I0l3Xy0qklwsvvNkqYnpX5mUg2eYYVUwafiW7lwApmOAzjQuYME78kRfkqXwu8gqMzmB0IK0jw3by_ZjGbYaadv3HaIzzmjs1HsAvTwvoZ4KKF-jdk5i7LevoGXrsHqeSKmM2vx5CJA0E5O_vIy5mHqU-ZxBL1v5EOdBS7QfpUVlMuByCMCq0xoww7Ygz458rLRw0sTK_jD563EfWZefePbFLI9ApTe1ULTxbZxaFNomktUKifrFruk8lyh7i9UykoKLV_Cl_MmOc1AYO_YPrg5irtMC-oHwE5xgyonjdYq4hJcgMoy5R-oUYnAiGnDyTHhMCPJ-Lmzr0kWo6v7S7VIr4mFXpqNN7isQtcMoTFd2MmYfaWyzGuTuk4xBXhilyO0OIo0_XPauD8eIC-LXyNd7l67ENTs_1iXCzOTzf_NKiXLlThXY7aA2XOOnJcQVLBIpjVRRk2aWezOIaakpXkg0HGiUzdBBttCXEZoSF68IftUXJVrmkS_oW3w_4dcIAvUeUb6opCDXcmg09fNwJgIRs",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "openid offline_access contact nickname",
"channels": "notifications product_updates",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJJcGJmaDRmcHFNcEhuMkM3V3hyS1EifQ.eyJqdGkiOiJEQ1oyXzdTZTN6aVhkY2RTc2NBTDB3IiwiaXNzIjoiaHR0cHM6Ly9wcm9qZWN0YWxpYXMuY29tIiwiYXVkIjoiYmpPQjZIVjdjVEt0eHZmcjJrMnVjZyIsImlhdCI6MTU5MzUxOTMzMCwiZXhwIjoxNTkzNTIxMTMwLCJub25jZSI6InJnbnhhSEY5eklIYWFmVm80XzhQZHciLCJhdF9oYXNoIjoiaU11S2FWb3ZRU29qNHVlMHN6RGlkZyIsInN1YiI6ImVwR0VjR2ZjNkxRTW1LVHFmZldXMmciLCJuaWNrbmFtZSI6IkZyZWQiLCJhdXRoX3RpbWUiOjE1OTM1MTg3MzB9.RPRBN6R7Ey6At6VtCdv_8qoS-0wQCWF6OQS7bUudt9IV01X1a5qKc1ja613u9lD0T5QNdVSZdwj6OYZ2LZ5QMRX_BDF42ZeyHhLSJ30RGyAOJoUDRIOT8CtX7fMOwmB52bh5Moo3XSJhjJ5pQ2S0hWQFIGi2eG3-CMYx0mLz8TmPoYi1AQT__Gvruc5JOcu9RvKHWBtT3SKvVZGKXxTb6KPnWbn0jnZGtQzDl3QWu6CTwQOXy_kQwqAsX94BIbHqN4hJZjplKf7OxElmNT0K0lCsDzpW6pKrK8_T0PbLTiJmlkKz3H6Ogb272r5v0sgvZqN_bNQv_2UzT5QSPrp6fxrfmhuPs3bcR6VWFtX2FOobzPJNj8WhelhgBXUp7Up3CqvcMDo3_CpMQlnkIOQoWcMRebzEBK4D8mNdi49CVzSguxtU6FjQVenwDhnZ5tGx6-hi34dlqu3zFEgOT19fnQX3ew6q8KvzQYbFdEv__TQuld5HTtrc_3hWynuS83e9",
"refresh_token": "qBRtkoK4AN1tKZkJnSMSHLsgjB9ToFz8FCn17TI3lF2dhmLpACoE_zZmBhiBc1YvnaLi-WXHHWMBm8pcNj3ln5So-fSKjOvYoZCSbKykzsCWtIEnlSLCLuYTt7O2ulSaUuowryBcdlgsF0LWzUd0BJ7leawLyz2ZvS37AzLrQGku0phaJiuwlmY2lD7iNuPxU4mwX2cfhK21OUHVvrBBc65t7Sf9AHFUX4lnM17PuzxNsXgL308Nq06Qluq7UF1u7mPuFqb6khudsbubCY0FcUNbrX1aEdObb4kdNP87T09DwQdvhdJZfBSrh_aKzxvcioFuvtfE9cxk2bfHUlCZNLLxuJwAJ3MdsvG1MHfIWQbI3WTdOQJ8iMeeTwS13UaQm1rkETkiMbGkIb5AJskxtWABF9N2Z645HTIJSWI9iThy2bJaZaK5dhmJNnZIqb6FbR4KTlpypo-hzdvI_gRXScWFDlfrYQc-Bz_q1VnZaSUChkDg7RzkIPEbEEHNc8fc26bOgQil-vTNM00RQUFTap81iiedOwRy3T8yIeY7VAY9FGvJDmWu7Msnf1aBOnAmhBNjJHzOCJOCsR_J9GPgL7BH2O9lfXf7Jj8HW-ez5rLj_OgygvKE_2HXBXrxcm7Kst8DnGSROO010CpWpm9C9S046TaimYZBoy1DIoz_xBA"
}
Refresh Token Request¶
POST https://projectalias.com/api/oauth2/token
Exchange a refresh token for new tokens. New user and refresh tokens are returned - new ID tokens are not. Only confidential clients can use this request.
By default, the new user token will have the same scopes as the user token from the original auth request. However, if the user has since revoked some of those scopes, they won't be included.
If the new user token only needs a subset of these scopes, this can be specified with the scope
field. If this includes any scopes that weren't one of the original scopes - or that have subsequently been revoked - the request will fail.
The updated refresh token returned always has the same functionality (i.e. can produce user tokens with the same scopes) as the one used - just with a later expiry.
See the OAuth 2.0 spec for more details on refreshing access tokens.
Authentication
The client must authenticate themselves with HTTP basic auth, using their client credentials. The client ID is used as the user-id
, and the client secret as the password
.
Warning
This request should only be performed from a secured backend as it requires sensitive client credentials.
Request Body
The request body must be encoded as application/x-www-form-urlencoded
, and the Content-Type
header set appropriately.
Field | Required | Description |
---|---|---|
grant_type |
Yes | Must be refresh_token |
refresh_token |
Yes | A refresh token obtained from a previous request to the token endpoint. |
scope |
No | A space separated list of scopes to request. |
Response Body
The response body is encoded as application/json
.
Field | Type | Description |
---|---|---|
access_token |
string |
A client token. |
token_type |
string |
Always Bearer . |
expires_in |
integer |
How many seconds the token will be valid for. Tokens are valid for 30 minutes. |
scope |
string |
A space separated list of scopes the token has. |
channels |
string |
A space separated list of channels the token has. Only included if the token has the contact scope. |
refresh_token |
string |
An updated refresh token. |
Errors
In case of an error, the endpoint will endevour to respond with an appropriate HTTP status code. It may include details of the error in an application/json
response body.
Field | Type | Description |
---|---|---|
error |
string |
An error code. |
error_description |
string |
An optional human-readable description of the error. |
See the relevant section of the OAuth 2.0 spec for details of the OAuth 2.0 specific error codes. Note that you may receive error codes not defined in the spec.
Examples
Example Request
POST /api/oauth2/token HTTP/1.1
Host: projectalias.com
Authorization: Basic <basic-credentials>
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=qBRtkoK4AN1tKZkJnSMSHLsgjB9ToFz8FCn17TI3lF2dhmLpACoE_zZmBhiBc1YvnaLi-WXHHWMBm8pcNj3ln5So-fSKjOvYoZCSbKykzsCWtIEnlSLCLuYTt7O2ulSaUuowryBcdlgsF0LWzUd0BJ7leawLyz2ZvS37AzLrQGku0phaJiuwlmY2lD7iNuPxU4mwX2cfhK21OUHVvrBBc65t7Sf9AHFUX4lnM17PuzxNsXgL308Nq06Qluq7UF1u7mPuFqb6khudsbubCY0FcUNbrX1aEdObb4kdNP87T09DwQdvhdJZfBSrh_aKzxvcioFuvtfE9cxk2bfHUlCZNLLxuJwAJ3MdsvG1MHfIWQbI3WTdOQJ8iMeeTwS13UaQm1rkETkiMbGkIb5AJskxtWABF9N2Z645HTIJSWI9iThy2bJaZaK5dhmJNnZIqb6FbR4KTlpypo-hzdvI_gRXScWFDlfrYQc-Bz_q1VnZaSUChkDg7RzkIPEbEEHNc8fc26bOgQil-vTNM00RQUFTap81iiedOwRy3T8yIeY7VAY9FGvJDmWu7Msnf1aBOnAmhBNjJHzOCJOCsR_J9GPgL7BH2O9lfXf7Jj8HW-ez5rLj_OgygvKE_2HXBXrxcm7Kst8DnGSROO010CpWpm9C9S046TaimYZBoy1DIoz_xBA
curl -XPOST "https://projectalias.com/api/oauth2/token" \
-H "Authorization: Basic <basic-credentials>" \
-d "grant_type=refresh_token" \
-d "refresh_token=qBRtkoK4AN1tKZkJnSMSHLsgjB9ToFz8FCn17TI3lF2dhmLpACoE_zZmBhiBc1YvnaLi-WXHHWMBm8pcNj3ln5So-fSKjOvYoZCSbKykzsCWtIEnlSLCLuYTt7O2ulSaUuowryBcdlgsF0LWzUd0BJ7leawLyz2ZvS37AzLrQGku0phaJiuwlmY2lD7iNuPxU4mwX2cfhK21OUHVvrBBc65t7Sf9AHFUX4lnM17PuzxNsXgL308Nq06Qluq7UF1u7mPuFqb6khudsbubCY0FcUNbrX1aEdObb4kdNP87T09DwQdvhdJZfBSrh_aKzxvcioFuvtfE9cxk2bfHUlCZNLLxuJwAJ3MdsvG1MHfIWQbI3WTdOQJ8iMeeTwS13UaQm1rkETkiMbGkIb5AJskxtWABF9N2Z645HTIJSWI9iThy2bJaZaK5dhmJNnZIqb6FbR4KTlpypo-hzdvI_gRXScWFDlfrYQc-Bz_q1VnZaSUChkDg7RzkIPEbEEHNc8fc26bOgQil-vTNM00RQUFTap81iiedOwRy3T8yIeY7VAY9FGvJDmWu7Msnf1aBOnAmhBNjJHzOCJOCsR_J9GPgL7BH2O9lfXf7Jj8HW-ez5rLj_OgygvKE_2HXBXrxcm7Kst8DnGSROO010CpWpm9C9S046TaimYZBoy1DIoz_xBA"
import requests
r = requests.post('https://projectalias.com/api/oauth2/token',
headers={'Authorization': 'Basic <basic-credentials>'}
data={'grant_type': 'refresh_token',
'refresh_token': 'qBRtkoK4AN1tKZkJnSMSHLsgjB9ToFz8FCn17TI3lF2dhmLpACoE_zZmBhiBc1YvnaLi-WXHHWMBm8pcNj3ln5So-fSKjOvYoZCSbKykzsCWtIEnlSLCLuYTt7O2ulSaUuowryBcdlgsF0LWzUd0BJ7leawLyz2ZvS37AzLrQGku0phaJiuwlmY2lD7iNuPxU4mwX2cfhK21OUHVvrBBc65t7Sf9AHFUX4lnM17PuzxNsXgL308Nq06Qluq7UF1u7mPuFqb6khudsbubCY0FcUNbrX1aEdObb4kdNP87T09DwQdvhdJZfBSrh_aKzxvcioFuvtfE9cxk2bfHUlCZNLLxuJwAJ3MdsvG1MHfIWQbI3WTdOQJ8iMeeTwS13UaQm1rkETkiMbGkIb5AJskxtWABF9N2Z645HTIJSWI9iThy2bJaZaK5dhmJNnZIqb6FbR4KTlpypo-hzdvI_gRXScWFDlfrYQc-Bz_q1VnZaSUChkDg7RzkIPEbEEHNc8fc26bOgQil-vTNM00RQUFTap81iiedOwRy3T8yIeY7VAY9FGvJDmWu7Msnf1aBOnAmhBNjJHzOCJOCsR_J9GPgL7BH2O9lfXf7Jj8HW-ez5rLj_OgygvKE_2HXBXrxcm7Kst8DnGSROO010CpWpm9C9S046TaimYZBoy1DIoz_xBA'})
Example Response
{
"access_token": "jsbt16o__lp5Jq22y0P75fU-tV8frztJwUJM7AfbGVusjQWoMCqWHdpBwTrsJ6xU5AYXvfS5cykuls3Dl7eBQ4Pu6_xcJGVxgmKEgBGmZJYH_b1xnkDSUvYEdqEuRUGujx-SQuvSjjWo4ABW-6BvM-yCUlS32RLLYdzFeF9brzvzJ9bE3bZOJBEGRufxG_DVI9zgkENpvp8CMKEjiOnaCjHOmU8y39ZDjVv8InF8BPCMJANi84YJtAkOjATD8dTZgwLpTQnnIXiS4l4bsVkWhUBYBRDSvicgVAuPGgTiOO251KEJMjS33zYU7PRmrqUSOIRSN0oNrzv4w2SQum01_gIwHwbkZgyyfIHMpyDedYX7LyWiird8bEElGGs00QwUcNR13RZrKBtAfgZAMsC7BxcGQu3S4_HfaPNDlLtqyzGTNgl1IOnkB22QfH_-dlRx8bZ2DMHgQMhiNi3Wmxy02V4eU2aMAeYBkG0sLxTHDT2IZnLP7dGWVJ_PGi7wPtVgE3wW1uK_ZqJIlKp0h315Z1VHf5JwF4cYrtg9HPZpXVE7VxxMhJCZ04yP9w2h_ys_q12-9vc4m0pt-hM3WTM2lVMU1S9WXmRjk_3m7lnbguFoKszhGm1py8XapmQE6YQZgwvvRcakFg7Dg2WPo-Zwrksxs1GTYtT57qR1OF7vwh8",
"token_type": "Bearer",
"expires_in": 1800,
"scope": "openid offline_access contact nickname",
"channels": "notifications product_updates",
"refresh_token": "LwYSrfsVB8vqbxGyZg_M9xUe1cEqL24xYC_9PTYIPc9Bj3tPVii7UIVbbR3VZyT63GHqgY6rwW05_Qr89wjhQ4fU94YK91_HO2iCqUM4Swn0qN3Nql43vb6o6ETgotVzuYsYGysn5fmgL6tO-zqKRPjU6uuW05iLjfv0eDZ6CAA_--9OJPYJux_DTBYfg9aXqRzuQyKPGLOfQBCowD3wjIrCHpbBLhuiZsITL-uZ9WsqLKfUAZceMDGoDP3Ycu4Jm5fzTPFu2UGtSKIbuYirr3yv_pDX7MASYs-k5LqKK16KVV2I_9nlQqNY_Qg-2n9J5SNzy0e4g5pe1CXyY9qNiilhqK-8D5LIxdCaPoA15710SCxUsUMwLAw-6WDeXRqf05CDyjk6vQGxviSYWboPMrynM3Dhp-sGVanELRb8u6R6QibpcfzqH9sJ-IhwwjnwFiBAaeck9wah4EfOdAAV3jK6w4Z0bW9dqvfccik4NKhio0xdpgzcteWlTXzAQPsUMdVKqO-nWVzwlWKO1szAIHkgMorrHaVND-vik2y8YmiGLny-Q5_QYT5OObgX_r9BfWFxoGI8imwfnL7OPuxIxBMuW6D8OP4HMrWeeVZCk89jrdbk34rHDEaogevX68VogofuqJDMxDwbrWz_hfbCpPrKT0WDnOY6cJZ3HJcNHQA"
}