Securing Micro Services in Quarkus with Amazon Cognito

Iain Porter
Level Up Coding
Published in
6 min readOct 6, 2020

In a previous series that I wrote on building a micro service from the ground up with Quarkus and Kotlin, the service was secured using OpenID Connect. The premise was that the service would be behind an API gateway and would be invoked by another micro service using JWTs issued by an OIDC provider. For component tests we used Keycloak as the provider, which also is a good choice for hosting your own OIDC server. We also demonstrated how Okta could be used as an external OIDC provider for issuing JWTs. In this article we will explore using Amazon Cognito.

What is Amazon Cognito

Cognito is a cloud-based identity and access management solution. It supports OAuth2 and OpenId Connect. It can be a cost-effective way to manage a large user base for your platform. It is comprised of two main components:

User Pools

A user directory in Cognito that provides all the security services that you would expect for managing users such as sign up, sign in, MFA, social login, user management, etc. Cognito will normalise any tokens received via federated login and return Cognito User Pool (CUP) tokens. These are just standardised JWTs so you don’t have to worry about the original format of each type of token.

Identity Pools

Allows access to AWS services via federated identities. Using identity pools you can obtain temporary access tokens to interact with AWS services. This is illustrated in Step 2 and Step 3 in the diagram below.

Securing Service to Service access

It is a common feature in micro services that they need to communicate with each other. This can be event based where one service emits an event and one or more services subscribe to receive the event. Another approach is to use REST, in which case the calling service will need to present an access token. This token is obtained using the client credentials flow. We can use Cognito to issue these tokens. For this scenario we only need the User Pool component as illustrated below.

Setting up AWS CLI

If you are already familiar with using the CLI set up you can skip this section. Using the CLI we can set up the User Pool and other components that we need to issue tokens.

If you have not yet installed the CLI you can follow the directions here

Before we can use the CLI we need to set up a user account in the AWS Web dashboard. Navigate to IAM -> Users -> Add User

Create the user, select “Programmatic Access” and assign the relevant permissions.

Using the credentials that are returned add them to the config file at

~/.aws/credentials[foo_profile]
aws_access_key_id = AK*********
aws_secret_access_key = it**********

Next add a new profile for these credentials at

~/.aws/config[profile foo_profile]
region = us-west-2
output = json

When invoking the CLI you can reference the profile by using the profile option. i.e.

> aws <command> --profile foo_profile

or you can set an environment variable

$ export AWS_PROFILE=foo_profile

Creating a User Pool

The Cognito User Pools API can be found here

To create a new User Pool execute the following command

$ aws cognito-idp create-user-pool --pool-name backend-client

This will create the user pool and return a json payload listing the schema attributes as well as other information. We need to get the id for use elsewhere. To get a short summary you can use the following command

$ aws cognito-idp list-user-pools --max-results 20

This will return a result similar to this

{
"UserPools": [
{
"CreationDate": 1601651956.335,
"LastModifiedDate": 1601651956.335,
"LambdaConfig": {},
"Id": "us-west-2_Knm7CFToH",
"Name": "backend-client"
}
]
}

Creating a Resource Server

Next we have to create a resource server that represents the resources that we want the users in the user pool to access, along with any scopes. For this use case we have a simple SMS service. The identifier can be any unique value for the resource server. We are assigning just one scope.

$ aws cognito-idp create-resource-server --name sms-service --identifier sms.porterhead.com --user-pool-id us-west-2_Knm7CFToH --scopes ScopeName=sms, ScopeDescription=send_sms

The response looks like this

{
"ResourceServer": {
"Scopes": [
{
"ScopeDescription": "send_sms",
"ScopeName": "sms"
}
],
"Identifier": "sms.porterhead.com",
"Name": "sms-service",
"UserPoolId": "us-west-2_Knm7CFToH"
}
}

Creating a Client

We need to create the client that we will use to invoke the sms service. The client will use the client credentials grant.

$ aws cognito-idp create-user-pool-client --user-pool-id us-west-2_Knm7CFToH --allowed-o-auth-flows client_credentials --client-name backend-service --generate-secret --allowed-o-auth-scopes porterhead.com/sms --allowed-o-auth-flows-user-pool-client

From the response we can extract the secret

{
"UserPoolClient": {
"ClientSecret": "16a8************************",
"AllowedOAuthScopes": [
"porterhead.com/sms"
],
"UserPoolId": "us-west-2_*******",
"AllowedOAuthFlowsUserPoolClient": true,
"LastModifiedDate": 1601899598.238,
"ClientId": "3e58***************",
"AllowedOAuthFlows": [
"client_credentials"
],
"RefreshTokenValidity": 30,
"CreationDate": 1601899598.238,
"ClientName": "backend-service"
}
}

Creating a Domain

We have to create a domain server to issue the tokens. You have the option of delegating to your own custom domain or using an Amazon Cognito hosted domain. The domain name should be unique across Amazon.

aws cognito-idp create-user-pool-domain --domain porterhead --user-pool-id us-west-2_Knm7CFToH

Once the domain is created and made ready it will be available at a unique url with your domain name prefix

https://porterhead.auth.us-west-2.amazoncognito.com/oauth2/token

Configuring Quarkus

As we discovered in a previous post setting up Quarkus to use OIDC is quite straightforward. In fact we don’t have to do anything to the application, we just need to modify the runtime properties to point to the new OIDC provider.

Quarkus will take care of the call backs to verify the JWT. We need to tell it where to get the OIDC configuration.

In the configuration file we are pointing to an Okta auth server

quarkus.oidc.auth-server-url=https://dev-****.okta.com/oauth2/*********

We can change this to point to the Cognito server

quarkus.oidc.auth-server-url=https://cognito-idp.us-west-2.amazonaws.com/us-west-2_Knm7CFToH

Quarkus will append ‘.well-known/openid-configuration’ path to this URL

https://cognito-idp.us-west-2.amazonaws.com/us-west-2_Knm7CFToH/.well-known/openid-configuration

We can check this URL to look at the properties returned

{
"authorization_endpoint": "https://porterhead.auth.us-west-2.amazoncognito.com/oauth2/authorize",
"id_token_signing_alg_values_supported": ["RS256"],
"issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_Knm7CFToH",
"jwks_uri": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_Knm7CFToH/.well-known/jwks.json",
"response_types_supported": ["code", "token"],
"scopes_supported": ["openid", "email", "phone", "profile"],
"subject_types_supported": ["public"],
"token_endpoint": "https://porterhead.auth.us-west-2.amazoncognito.com/oauth2/token",
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
"userinfo_endpoint": "https://porterhead.auth.us-west-2.amazoncognito.com/oauth2/userInfo"
}

Let’s also check that the jwks_uri returns what we expect

{
"keys": [{
"alg": "RS256",
"e": "AQAB",
"kid": "0x5vxTf0YCGHCkEGxgrU3lY/I9H0+DbhRfyq3249mt8=",
"kty": "RSA",
"n": "i9UJRoeUrm5ErIbO73K6rFAo4lgsKSj6LF1VrUqIn38UzXSl0pFA5chluMfjPaBSddgdbOA7EOD4ws3LknQy-aDWq3BvN1FSUgTjQZitejgshJboxv2fweh-gRMh1mIqxrL3cFCg_6OFknmWPLPdlxAR8LLBrdFkq5YrpIPNrF1wzgoQ-yy29IE0gvOsEMJ8Y3FZ7F65kieU-IiSzC1YEvVzSNtDMDzf0bDdwkSoFvQ2wj7-6F04g7zgJIolsuxzR1kTefVBEoNB8Du-byAWdcPm3GMEmXQngoFTmGatHwsrIiHXxDvRNj9UBt8uf2dpqkcGVpzKcYlCQjyXIQAmUQ",
"use": "sig"
}]
}

Once the properties are amended we can start everything up using the instructions here and invoke the service once we have obtained an access token.

Obtaining an Access Token

With client credentials we need to Base64 encode the credentials and use that in the header.

echo -n '<clientId>:<secret>' | openssl base64 -Acurl -X POST \
https://porterhead.auth.us-west-2.amazoncognito.com/oauth2/token \
-H 'authorization: Basic M2U1OHMwO******************' \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&scope=porterhead.com%2Fsms'

We should get back a valid token

{"access_token":"eyJraWQiOiIweDV2eFRmMFlDR0hDa0VHeGdyVTNsWVwvSTlIMCtEYmhSZnlxMzI0OW10OD0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIzZTU4czA5YnJvamRvbHNlcjRsMDd0dmY0ayIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoicG9ydGVyaGVhZC5jb21cL3NtcyIsImF1dGhfdGltZSI6MTYwMTkwMjQwNSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLXdlc3QtMi5hbWF6b25hd3MuY29tXC91cy13ZXN0LTJfS25tN0NGVG9IIiwiZXhwIjoxNjAxOTA2MDA1LCJpYXQiOjE2MDE5MDI0MDUsInZlcnNpb24iOjIsImp0aSI6IjNjZjRjOGFlLWFjYTctNDQ3Yy04MmIzLWMwYjY3ZjhjNzM4ZCIsImNsaWVudF9pZCI6IjNlNThzMDlicm9qZG9sc2VyNGwwN3R2ZjRrIn0.dZR9tNAqqx3eDWTufM6ZAtG30c7LljqmvcVG-mLx21ivaYteqktdWitRfXZYLiU_y85ya4oqUy7y4C8Yu_UIfq6SxIUiilm_JyQ4WkZ_iKfsYV1UKmQl7V_ppSwyAleYW-L2b1YDnlNaUBhXQZLr0S_fzHJuAruHWLUzEDch6unv2XEp63SAtZb0tcUlkiPZ5LzaSKlPqFX-SWGF_abilUggrRGbxc6Z35Rwh6UGx1Q2A8JEgs3Zwbh8CTSKquLKf6Dw7KUEz6GXkc0J1EMlvQkjs8fLOi09hVe9cGn8yJ9j1eIasZm17LoIbo34nMa9ocpIAC0ThGzH6ZxuMqGT6Q","expires_in":3600,"token_type":"Bearer"}

If we stick that in jwt.io we can see the payload and that it also used the public key that we expected

We can use this token to invoke the service

curl 'http://localhost:8080/v1/sms' -i -X POST  \
-H 'Content-Type: application/json' \
-H 'authorization: Bearer <your access token>'
-d '{"text":"Foo Bar!", "toNumber": "<your SMS number"}'

This is just scratching the surface of how you can use Amazon Cognito to manage AuthZ and AuthN in your micro services platform. It is a worthy competitor to the growing number of IAMs available.

--

--

Written by Iain Porter

Technology pathfinder who likes to code when not on his bike

Responses (1)