Table of Contents
"Amazon Cognito user pools implements ID, access, and refresh tokens as defined by the OpenID Connect (OIDC) open standard" — excerpted from “Using Tokens with UserPools”
However, because the OIDC implementation of Cognito is very limited and inflexible, it is common that Cognito’s OIDC implementation cannot satisfy requirements of your system. For example, the signature algorithm of ID tokens issued by Cognito is RS256
and there is no way to change it although the algorithm is prohibited by Financial-grade API (FAPI) for security reasons.
This tutorial explains how to use Cognito just as a user database and delegate OAuth/OIDC-related tasks to Authlete so that your system can continue to use Cognito and at the same time support the latest OAuth/OIDC specifications such as Financial-grade API (cf. Authlete Spec Sheet).
In the OAuth 2.0 context, a server that issues access tokens (and optionally refresh tokens) is called authorization server. On the other hand, in the OpenID Connect context, a server that issues ID tokens is called OpenID Provider (IdP). Because OIDC has been defined intentionally on top of OAuth 2.0, it is common that one server has both the roles. Therefore, the same server may be differently called “authorization server” or “IdP” depending on contexts. This tutorial refers to the server as “authorization server” uniformly.
To support the authorization code flow (RFC 6749 Section 4.1), which is the most common flow in OAuth/OIDC, an authorization server has to implement two endpoints. They are called authorization endpoint (RFC 6749 Section 3.1) and token endpoint (RFC 6749 Section 3.2). Cognito User Pool provides implementations of the two endpoints, but you need to implement your own custom endpoints when Cognito’s OIDC implementation is not satisfactory.
The diagram below illustrates the relationship among components in the authorization code flow when Cognito and Authlete are used combinedly.
The point in the diagram is that user authentication is performed by Cognito but OAuth/OIDC-related tasks are delegated to Authlete. Considering the fact that the core specification of OAuth 2.0 (RFC 6749) states “The way in which the authorization server authenticates the resource owner (e.g., username and password login, session cookies) is beyond the scope of this specification.”, this clear separation brings about much more benefits than the approach where a user management solution directly supports OAuth/OIDC.
The following sections describe in detail what the authorization server prepared for this tutorial does in its implementations of the authorization endpoint and the token endpoint.
The authorization endpoint in the sample authorization server:
The token endpoint in the sample authorization server:
The architecture explained above is implemented in django-oauth-server, which is an open-source authorization server written in Python with the Django web framework. To run the server, follow the steps below.
email
attribute is included because we will use the attribute later for testing.ALLOW_ADMIN_USER_PASSWORD_AUTH
in the “Auth Flows Configuration” section of the client configuration so that the client can use the “Server-Side Authentication Flow”.Install necessary Python libraries.
$ pip install authlete # Authlete Library for Python
$ pip install authlete-django # Authlete Library for Django
$ pip install boto3 # AWS SDK for Python
Download the source code of the authorization server implementation.
$ git clone https://github.com/authlete/django-oauth-server.git
$ cd django-oauth-server
Edit the Authlete configuration file (authlete.ini
) to access Authlete APIs.
$ vi authlete.ini
Open the Django configuration file (django_oauth_server/settings.py
),
$ vi django_oauth_server/settings.py
and add backends.CognitoBackend
to AUTHENTICATION_BACKENDS
. See “Specifying authentication backends” in “Customizing authentication in Django” for details about Django authentication backends.
AUTHENTICATION_BACKENDS = ('backends.CognitoBackend',)
Also, edit COGNITO_USER_POOL_ID
and COGNITO_CLIENT_ID
in the same file properly.
COGNITO_USER_POOL_ID = 'YOUR_COGNITO_USER_POOL_ID'
COGNITO_CLIENT_ID = 'YOUR_COGNITO_CLIENT_ID'
If you are interested in how to call Cognito’s AdminInitiateAuth API and AdminGetUesr API, look into the source code cognito_backend.py
.
To start the authorization server, type the command below.
$ python manage.py runserver
“make run
” does the same thing.
$ make run
The authorization server exposes some endpoints as listed in the table below. An easy way to confirm that Authlete setup (authlete.ini
) is correct is to access the discovery endpoint (http://localhost:8000/.well-known/openid-configuration) and see if it returns JSON conforming to OpenID Connect Discovery 1.0.
Endpoint | URL |
---|---|
Authorization Endpoint | http://localhost:8000/api/authorization |
Token Endpoint | http://localhost:8000/api/token |
Discovery Endpoint | http://localhost:8000/.well-known/openid-configuration |
We have done all preparation. Let’s get an access token and an ID token by the authorization code flow.
In the authorization code flow, the first step is to send an authorization request to the authorization endpoint of the authorization server via a web browser. In this tutorial, the authorization endpoint is http://localhost:8000/api/authorization
hosted on django-oauth-server. Replace CLIENT_ID
and REDIRECT_URI
in the URL below (which represents an authorization request) properly and access the URL with your web browser.
http://localhost:8000/api/authorization?response_type=code&client_id=CLIENT_ID&scope=openid+email&state=123&nonce=abc&redirect_uri=REDIRECT_URI
Your web browser will display an authorization page generated by the authorization server. It will look like below.
The page has input fields for Login ID and Password. Input the username and the password of the user that you have added to the Cognito user pool there and press “Authorize” button, and your web browser will be redirected to the redirection endpoint of your client application.
The URL of the redirection endpoint which you can see in the address bar of your browser contains code
response parameter like below.
REDIRECT_URI?state=123&code=RwRq2Lp0bJVMiLPKAFz4qB1hxieBD1X5HKuv8EPkJeM
The value of code
response parameter is the authorization code which has been issued from the authorization server to your client application. The authorization code is needed when your client application makes a token request.
After getting an authorization code, the client application sends a token request to the token endpoint of the authorization server. In this tutorial, the token endpoint is http://localhost:8000/api/token
hosted on django-oauth-server.
A token request can be made by curl
command in a shell terminal. Below is an example of token request. Don’t forget to replace CLIENT_ID
, REDIRECT_URI
and CODE
with the actual values before typing.
$ curl http://localhost:8000/api/token -d grant_type=authorization_code -d client_id=CLIENT_ID -d redirect_uri=REDIRECT_URI -d code=CODE
Argument | Description |
---|---|
http://localhost:8000/api/token |
The URL of the token endpoint. |
-d grant_type=authorization_code |
indicates that the flow is the authorization code flow. |
-d client_id=CLIENT_ID |
specifies the client ID. Replace CLIENT_ID with the actual client ID of your client application. |
-d redirect_uri=REDIRECT_URI |
specifies the redirect URI. Replace REDIRECT_URI with the same value given in the authorization request. |
-d code=CODE |
specifies the authorization code. Replace CODE with the actual authorization code. |
When the token request succeeds, the token endpoint returns JSON that includes access_token
and id_token
like below.
{
"access_token": "FrGIJQpW51-l5mYJHcqGUNIKGJ1W23fFlW6c9AQEZEc",
"refresh_token": "jhvKm9-haLQwnIR4CfkL6bfPIBBlqluFeqZKAgdPNjM",
"scope": "email openid",
"id_token": "eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRha2FAYXV0aGxldGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsInN1YiI6IjIiLCJhdWQiOlsiNDMyNjM4NTY3MCJdLCJleHAiOjE2MTY0MTI3NDIsImlhdCI6MTYxNjMyNjM0MiwiYXV0aF90aW1lIjoxNjE2MzI2MTAwLCJub25jZSI6ImFiYyIsInNfaGFzaCI6InBtV2tXU0JDTDUxQmZraG43OXhQdUEifQ.7sXy2FcELxHo3LCQkb9teLaUE9jtRxXsa8diJKnkwAo",
"token_type": "Bearer",
"expires_in": 86400
}
The value of access_token
is the issued access token. Likewise, the value of id_token
is the issued ID token.
The payload part of the issued ID token in this tutorial is decoded as follows. We can confirm that the authorization server has communicated with the Congnito user pool successfully by checking whether the value of email
in the payload matches the email
attribute of the user in the Cognito user pool.
{
"email": "taka@authlete.com",
"iss": "https://example.com",
"sub": "2",
"aud": [
"4326385670"
],
"exp": 1616412742,
"iat": 1616326342,
"auth_time": 1616326100,
"nonce": "abc",
"s_hash": "pmWkWSBCL51Bfkhn79xPuA"
}
You’ve completed this tutorial and learned how to enable your authorization server to use Amazon Cognito as a user database and at the same time support the latest OAuth/OIDC specifications by using Authlete.
Contact us if you need support. You are always welcome!