Sharing Our Passion for Technology
& Continuous Learning
Node Reference - Cognito Setup
Cognito setup
In order to secure our application we are going to leverage OpenID Connect{:target="_blank"}. Each request to our application from either another service or a logged in human user will contain a JSON Web Token (a.k.a. JWT){:target="_blank"} as a "Bearer" token in the Authorization{:target="_blank"} header{:target="_blank"}. This token not only proves who the client is, but it also holds information about that client (e.g. its Claims) that include email address and roles.
Great application security is difficult. This is why we believe in following a best practice of offloading as much security related design to industry standards and as much implementation as possible to community reviewed and trusted third party libraries.
In order to support OpenID Connect we need an Identity Provider
. This is a server that both the client and our application / service trust. The client sends credentials (i.e. Client ID and Secret pair) in order to obtain a JWT{:target="_blank"} and the application / service uses the public key of the Identity Provider
to verify the signature of the JWT{:target="_blank"}. If your organization already has an OpenID Connect compatible identity provider in place, then reach out to the responsible team to inquire about using it as the provider for your application. This will allow single sign-on for users that already have accounts at that Identity Provider
. If not, an AWS Cognito User Pool{:target="_blank"} is OpenID compatible. Unfortunately, the Cloudformation support for User Pools is lacking the ability to configure the resources we need so we will have to do this configuration via a combination of CloudFormation and the AWS CLI.
Start by placing the following in a cognito.template.yaml
file. This will specify your CloudFormation stack for the following AWS resources:
- a Cognito UserPool
- a Cognito UserPoolClient
AWSTemplateFormatVersion: '2010-09-09'
Description: Pipeline for Organization's Cognito UserPool
Parameters:
AuthName:
Type: String
Description: Unique Auth Name for Cognito Resources
Resources:
UserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
UserPoolName: !Sub ${AuthName}-user-pool
MfaConfiguration: 'OFF'
AutoVerifiedAttributes:
- email
Schema:
- Name: sub
AttributeDataType: String
Mutable: true
Required: true
StringAttributeConstraints:
MinLength: '1'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: name
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: given_name
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: family_name
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: middle_name
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: nickname
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: preferred_username
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: profile
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: picture
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: website
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: email
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: email_verified
AttributeDataType: Boolean
Mutable: true
Required: false
DeveloperOnlyAttribute: false
- Name: gender
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: birthdate
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '10'
MaxLength: '10'
DeveloperOnlyAttribute: false
- Name: zoneinfo
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: locale
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: phone_number
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: address
AttributeDataType: String
Mutable: true
Required: false
StringAttributeConstraints:
MinLength: '0'
MaxLength: '2048'
DeveloperOnlyAttribute: false
- Name: updated_at
AttributeDataType: Number
Mutable: true
Required: false
NumberAttributeConstraints:
MinValue: '0'
DeveloperOnlyAttribute: false
Policies:
PasswordPolicy:
RequireLowercase: true
RequireSymbols: true
RequireNumbers: true
MinimumLength: '8'
RequireUppercase: true
# All attributes are readable and writable by default because
# no "ReadAttributes" or "WriteAttributes" are specified.
Outputs:
UserPoolId:
Value: !Ref UserPool
Export:
Name: 'UserPool::Id'
Then deploy your cognito.template.yaml
stack via the AWS CLI{:target="_blank"}:
AUTH_NAME="theproducts"
aws cloudformation deploy \
--stack-name=Cognito \
--template-file=cognito.template.yml \
--parameter-overrides AuthName="$AUTH_NAME" \
--capabilities CAPABILITY_IAM
Next, you can specify the domain of your User Pool{:target="_blank"} via the AWS CLI, since CloudFormation does not support this property at this time.
# This looks up our userpool id.
# You can also simply copy the output variable from the cognito stack via the AWS Console
USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name Cognito \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' \
--output text)
# Because cloudformation does not yet support domains, we can create it via the CLI
aws cognito-idp create-user-pool-domain --domain "$AUTH_NAME" --user-pool-id "$USER_POOL_ID"
The Cognito user pool is the shared component (this is why it is deployed as its own template). Each service we build will need its own Resource Server{:target="_blank"}. This is the container for the scopes (or roles) that our service is secured by.
We also need a service account for our service so that the acceptance tests have access to call our APIs. UserPool Client{:target="_blank"} is used for this purpose. Resource servers cannot yet be created via Cloudformation so we will have to use the CLI:
# unique identifier for our resource server. The fully qualified domain name is a good choice
SERVICE_IDENTIFIER="products.example.com"
# Create a resource server
aws cognito-idp create-resource-server \
--user-pool-id "$USER_POOL_ID" \
--identifier $SERVICE_IDENTIFIER \
--name "Product Service" \
--scopes \
"ScopeName=products:read,ScopeDescription=Product Read Access" \
"ScopeName=products:write,ScopeDescription=Product Write Access"
# Create a client for our acceptance tests
USER_POOL_CLIENT_ID=$(aws cognito-idp create-user-pool-client \
--user-pool-id "$USER_POOL_ID" \
--client-name "product-acceptance-tests" \
--generate-secret \
--allowed-o-auth-flows-user-pool-client \
--supported-identity-providers "COGNITO" \
--allowed-o-auth-flows "client_credentials" \
--allowed-o-auth-scopes \
"$SERVICE_IDENTIFIER/products:read" \
"$SERVICE_IDENTIFIER/products:write" \
--query 'UserPoolClient.ClientId' --output text)
echo "Created User Pool Client: $USER_POOL_CLIENT_ID"
To test your new identity provider, you can use cURL{:target="_blank"} to attempt to obtain a token by executing the following commands:
CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client --user-pool-id "$USER_POOL_ID" --client-id "$USER_POOL_CLIENT_ID" --query 'UserPoolClient.ClientSecret' --output text)
curl -X POST \
https://${USER_POOL_CLIENT_ID}:${CLIENT_SECRET}@${AUTH_NAME}.auth.us-east-1.amazoncognito.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=client_credentials
You should receive a JSON response that contains an "access_token" field. This token is the JWT{:target="_blank"} that is sent to the server. It will look something like shown below:
{
"access_token": "eyJraWQiOiJvO......93t0Ei-hr9Ww",
"expires_in": 3600,
"token_type": "Bearer"
}
If you are curious what the token contains, it can be copy/pasted into jwt.io{:target="_blank"} to view its contents. It will look something like this:
{
"sub": "3op....11f",
"token_use": "access",
"scope": "products.example.com/products:read products.example.com/products:write",
"auth_time": 1528675981,
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_LJ****",
"exp": 1528679581,
"iat": 1528675981,
"version": 2,
"jti": "79b6....15",
"client_id": "3op....11f"
}
To verify the signature of our token we need to obtain the public key for our identity provider. Most identity providers rotate this key-pair on a frequent (ex. hourly) basis. To always obtain up to date public keys we can leverage the JSON Web Key Set or JWKS{:target="_blank"} standard. This specification provides a standard way to encode a set of public keys in a JSON document and then make it available at a URL. We can fetch the current JWKS document for our identity provider by executing this command with the correct cognito_user_pool_id
value (found as Pool Id
in the AWS Console in the User Pool's General Settings
):
curl https://cognito-idp.us-east-1.amazonaws.com/${USER_POOL_ID}/.well-known/jwks.json
We now have a fully managed, security standards-compliant identity service available for use. See the changes we made here{:target="_blank}.
Table of Contents
- Introduction
- Unit Testing
- Koa
- Docker
- Cloudformation
- CodePipeline
- Fargate
- Application Load Balancer
- HTTPS/DNS
- Cognito (this post)
- Authentication
- DynamoDB
- Put Product
- Validation
- Smoke Testing
- Monitoring
- List Products
- Get Product
- Patch Product
- History Tracking
- Delete
- Change Events
- Conclusion
If you have questions or feedback on this series, contact the authors at nodereference@sourceallies.com.