Cognito and OAuth2 from a clients’ point of view

Johannes Geiger
3 min readMar 16, 2024

--

Even though I can’t deny that since I learnt how easy Application Load Balancers (ALB) and Cognito integrate with each other I’m a huge fan of offloading authentication to an ALB sometimes it might not be necessary to go „all in“ and instead omit the ALB and directly use Cognito with a service like an API Gateway.

Whilst it’s not affecting the user experience, the biggest difference lies in the client-side implementation as the ALB will no longer perform the login post-processing and instead it’s now the responsibility of the client to obtain tokens. For this, we’re going to look at two scenarios — a basic and an extended workflow. After logging in as part of the basic workflow the client directly receives the ID token in the URL (which is potentially dangerous as every proxy between Cognito and the client might log the full URL including tokens for e.g. access logging purposes), in the extended workflow the client only receives a short lived code which needs to be exchanged with the tokens afterwards, additionally requiring a client secret which both increases the complexity on one hand but also mitigates the potential issues described above (as the code itself is not of any use).

Before we can take care of the tokens we set up a Cognito user pool and a mock API Gateway to verify that our process works end to end. For Cognito, just use the default values and for the API Gateway create a new REST API, create a Cognito authorizer referring to the user pool created above and the “Token source” Authorization , create a resource with a GET method backed by a Mock and use the Authorizer created previously in the “Method request settings” section. Deploy the API with a newly created stage and note the “Invoke URL”.

For the basic workflow the Cognito client will be created without a client secret, an OAuth2 grant type: Implicit grant and with an allowed redirect as the host our client is running on (e.g. http://localhost:3000). Opening $COGNITO_URL/login?response_type=token&client_id=$CLIENT_ID&redirect_uri=http://localhost:3000 (having replaced $COGNITO_URL and $CLIENT_ID with the respective values) in our Browser we’re seeing the login form and after logging in we’re getting redirect back and can extract the token from the URL.

For the extended workflow the Cognito client will be created with a client secret, an OAuth2 grant type: Authorization code grant (the default) and the same allowed redirect as above. Opening $COGNITO_URL/login?response_type=code&client_id=$CLIENT_ID&redirect_uri=http://localhost:3000 (having replaced $COGNITO_URL and $CLIENT_ID with the respective values again) in our Browser we’re again seeing the login form but after logging in we’re getting redirected back and can extract the code from the URL. Then, we need to do a second request to exchange the code against the token, e.g. via curl

curl -g -XPOST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u "$CLIENT_ID:$CLIENT_SECRET" \
$COGNITO_URL/oauth2/token"?grant_type=authorization_code&code=$CODE&redirect_uri=http://localhost:3000"

to receive the tokens. If the process gets implemented in JavaScript running in a Browser this second request might cause problems as Browsers usually don’t allow asynchronous requests across origins (CORS) but this can be circumnavigated by e.g. using NGINX as a reverse proxy.

Now that after both workflows we have an ID token we can finally call our API Gateway using the token receiving the expected response via e.g. curl:

curl -i $INVOKE_URL/$STAGE/$RESOURCE_NAME

should result in a 401 with {"message":"Unauthorized"} whilst

curl -i -H 'Authorization: Bearer '$ID_TOKEN $INVOKE_URL/$STAGE/$RESOURCE_NAME

should result in a 200 . I have further implemented both workflows in vanilla JavaScript communicating with AppSync afterwards as an alternative example also including the aforementioned NGINX reverse proxy pattern.

So, despite the fact that the pattern of directly interacting with Cognito is less elegant than the pattern incorporating the ALB implementing the workflow is not massively complex and a viable alternative for architectures that don’t need an ALB otherwise.

--

--