AWS Cognito provides a mechanism for both user authentication and user authorization within AWS managed applications. Cognito User Pools handle the authentication component, allowing one to integrate other Identity Providers (IDPs) which support SAML or OpenID Connect. Cognito Identity Pool provider authorization by delegating roles to both authenticated and unauthenticated users. This tutorial will go over how AWS Cognito can be used to authenticate user for a simple coffee-blog website where guest users may view articles and authenticated users may both submit and view articles.
I'm using the domain awscdkeg.net
I purchased through Route53 to host the
coffee application as using the hosted zone to create domain certificate.
/**
* Look up the hosted zone created using the registration process
*/
this.hostedZone = route53.HostedZone.fromLookup(
this,
"awscdkexamplehostedzone",
{
domainName: this.domainName,
}
);
this.domainCertificate = new acm.Certificate(this, "exampleCertificate", {
domainName: this.domainName,
validation: acm.CertificateValidation.fromDns(this.hostedZone),
});
A dynamo table will be used to store the submitted articles as items within the table. Each article will consist of the following information
- A unique UUID identifier
- The email of the user which submitted the article
- The article title
- The article content
Each of these will be kept as string attributes with the table's items. The CDK used to produce the dynamodb table.
this.articleTable = new dynamodb.Table(this, "articleTable", {
partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
encryption: dynamodb.TableEncryption.DEFAULT,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
User Pools provides an endpoint to authenticate users. From the perspective of your application, the User Pool is an OIDC IDP which issues identity tokens to users which successfully authenticate. The way this works is that you can add third-party IDPs to you User Pools as trusted identity providers. When a user attempts to authenticate with the User Pool, the User Pool will handle creating the authentication request to the third party identity provider as well as the response. The User Pool will then convert the response into an identity token JSON web token (JWT) as you would in a normal OIDC authentication transaction. The beauty of this is that even if you add to third-party IDPs that use different authenticate protocols, the User Pool will 'normalise' the authentication response as a JWT, meaning your application only ever to worry about needing to consume JWTs from the authentication process. Here is what a typical login and logout flows look like using Cognito with SAML authentication.
Login:
Logout:
To start we need to create a User Pool within CDK to act as our OIDC endpoint for authorising users.
this.userPool = new cognito.UserPool(this, "oktaSamlUserPool", {
userPoolName: "oktaSamlUserPool",
mfa: cognito.Mfa.OFF,
selfSignUpEnabled: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
For this tutorial, I'll add a third-party SAML IDP as a way for our User Pool to authenticate our users. To do this, I've created a free auth0 (owned by okta) web application and have enabled the SAML addon. We can add this as an identity provider through the following CDK code:
const oktaSamlIdentityProviderMetadata =
cognito.UserPoolIdentityProviderSamlMetadata.url(
// Change this our your own metadata URL!
"https://dev-npaajtq6i6vncnr2.us.auth0.com/samlp/metadata/EjjqseDMDm7vmlxjRO9AeT8YB7xuHI4e"
);
this.oktaSamlIdentityProvider = new cognito.UserPoolIdentityProviderSaml(
this,
"oktaSamlIdentityProvider",
{
userPool: this.userPool,
metadata: oktaSamlIdentityProviderMetadata,
idpSignout: true,
attributeMapping: {
email: cognito.ProviderAttribute.other("email"),
familyName: cognito.ProviderAttribute.other("family_name"),
givenName: cognito.ProviderAttribute.other("given_name"),
nickname: cognito.ProviderAttribute.other("name"),
},
}
);
this.userPool.registerIdentityProvider(this.oktaSamlIdentityProvider);
Adding the identity provider is required for Cognito to create SAML requests to our SAML IDP. The attribute mapping will map SAML claims from our SAML IDP to OIDC attributes.
The following configures a domain for you application to make OIDC requests to, you can alternative use a domain that you have purchased, otherwise AWS will otherwise generate one for you. Adding a generated domain is done so with the following CDK:
this.userPool.addDomain("okatSamlUserPoolDomain", {
cognitoDomain: {
domainPrefix: this.userPoolDomainPrefix,
},
});
A User Pool client is used to identify and interact with a web or mobile application. It provides information information on what the application is permitted to read and modify, which identity providers are permitted to authenticate users what api calls can be made to authenticate and unauthenticate and where to redirect users after authentication and unauthentication take place. The CDK code used to generate our coffee-app client is shown below. It has permissions to read all our our user's attributes and will use our SAML IDP to authenticate them.
this.oktaSamlClient = this.userPool.addClient("oktaSamlClient", {
userPoolClientName: "oktaSamlClient",
generateSecret: true,
oAuth: {
callbackUrls: [`https://${props?.domainName}/login`],
logoutUrls: [`https://${props?.domainName}`],
flows: {
// This is not recommended for production settings
authorizationCodeGrant: false,
implicitCodeGrant: true,
},
// When you authenticate user using the user pool OAuth 2.0
// authorization server to need to specify the scope of the attributes
// returned by the server.
//
// Here we are allowing all attributes the app client can read.
scopes: [
cognito.OAuthScope.OPENID,
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.PROFILE,
],
},
supportedIdentityProviders: [
cognito.UserPoolClientIdentityProvider.custom(
this.oktaSamlIdentityProvider.providerName
),
],
});
Identity pools allow us to grant AWS credentials to users on our website.
Different levels can be granted depending on whether the user is authenticated
or not as well as the values returned in the claims. In this example, we will
create an Identity Pool which provides different levels of permissions for
authenticated and unauthenticated users. The CDK code to create our Identity
Pool can be found within the bottom half of our CognitoStack
this.identityPool = new IdentityPool(this, "oktaSamlIdentityPool", {
// Allow the identity pool to automatically create the authenticated role
// and guest role since it's difficult to create the trust policies for
// these roles by hand. We can simply retrieve the automatically created
// layer and add our own policies to them.
identityPoolName: "oktaSamlIdentityPool",
allowUnauthenticatedIdentities: true,
authenticationProviders: {
userPools: [
new UserPoolAuthenticationProvider({
userPool: this.userPool,
userPoolClient: this.oktaSamlClient,
disableServerSideTokenCheck: false,
}),
],
},
});
This will create the authenticated and unauthenticated user roles with a pre-created trust policy which only allows Federated Web Identities that have authenticated through our User Pool client to assume these roles. As mentioned in the intro, we only want to provide unauthenticated users with read-only permissions to our article table. To this, we can create a new IAM policy with read-only permissions to the table and attach and add it to the pre-created unauthenticated Identity Pool user role.
const getCognitoCredentialsStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ["*"],
actions: ["cognito-identity:GetCredentialsForIdentity"],
});
const unauthenticatedUserStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [props.articleTable.tableArn],
actions: [
"dynamodb:BatchGetItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query",
],
});
this.identityPool.unauthenticatedRole.addManagedPolicy(
new iam.ManagedPolicy(this, "unauthenticatedManagedPolicy", {
statements: [
getCognitoCredentialsStatement,
unauthenticatedUserStatement,
],
})
);
We will do something similar with our authenticated user except the role will also grant put item permission to submit articles.
const authenticatedUserStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [props.articleTable.tableArn],
actions: [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query",
"dynamodb:UpdateItem",
],
});
this.identityPool.authenticatedRole.addManagedPolicy(
new iam.ManagedPolicy(this, "authenticatedManagedPolicy", {
statements: [
getCognitoCredentialsStatement,
authenticatedUserStatement,
],
})
);
Setting up the dynamodb client which uses these credentials to make API calls
takes place in the client-side app/app/root.tsx
file.
const idTokenString = Cookies.get("idToken");
const logins: Record<string, string> = {};
if (idTokenString) {
setDecodedJwt(jwtDecode(idTokenString));
logins[`cognito-idp.${env.REGION}.amazonaws.com/${env.USER_POOL_ID}`] =
idTokenString;
}
const createDynamoResources = async () => {
const creds = fromCognitoIdentityPool({
identityPoolId: env.IDENTITY_POOL_ID,
clientConfig: { region: env.REGION },
logins: logins,
});
setDynamodbClient(
new DynamoDBClient({
region: env.REGION,
credentials: creds,
})
);
};
createDynamoResources();
The identity token passed to the application duration when returning from
authentication is stored as a cookie a retrieved here. If the cookie is set,
it is passed in as login information to the fromCognitionIdentityPool
which provides credentials for a particular identity pool. If the logins
parameter is left as undefined
then credentials for the unauthenticated user
is provide. On the other hand, the identity token is present then credentials
for the authenticated user role is given.
First you will need to create a SAML identity provider. For this example I just used an auth0 developer account and used that to create an IDP for a web app.
First clone this repository
git clone https://github.com/Michae1CC/aws-cdk-examples
and change directory into the cognito-saml-identity-pools
folder.
cd cognito-saml-identity-pools
Run
npm install
to install the required packages to create our Cloudformation template. Next bootstrap and deploy your environment.
cdk bootstrap && cdk deploy
Make sure you have docker running during this set. Once deployed, you will need
to make adjustments to your SAML IDP. Ensure that the aud
attribute in
SAML responses is set to be urn:amazon:cognito:sp:<USER POOL ID>
. You can find the
User Pool ID from the USERPOOLID
Cloudformation Output value. You will also
need to follow the steps from here. This is what my auth0 app json
configuration for the SAML addon ended up looking like:
{
"audience": "urn:amazon:cognito:sp:<USER POOL ID>",
"mappings": {
"user_id": "user_id",
"email": "email",
"name": "name",
"family_name": "family_name"
},
"passthroughClaimsWithNoMapping": false,
"logout": {
"callback": "https://<USER POOL DOMAIN>.com/saml2/logout",
"slo_enabled": false
},
"signingCert": "<COGNITO SAML IDP SIGNING CERT>",
"signResponse": true
}
Finally, within the AWS console, navigate to your SAML IDP and make sure these boxes are selected (the Cloudformation Constructs I used to create the User Pool doesn't currently have the ability to control some of these properties, if you know an easy fix feel free to PR a solution!).
Once deployed we should be able to visit the website of our domain,
in this case awscdkeg.net
. As a unauthenticated user, your identity pool
will only give you permissions to view articles (stored as dynamodb entries).
You can see these articles in the right colum of the home page
We can login into our coffee, simple click the login button where the SAML IDP will issue you a challenge to authenticate you. I'm just using a simple user-name an password challenge to sign into my auth0 app.
The id_token
provided by your IDP will allow you to assume the
authenticatedManagedPolicy
that allows us to create items within our dynamodb
table. To create an item, navigatge the Create
link and fill out the sections
with the article information.
When we hit submit, a request will be made to create a new item with the article information. We can check that the item was created by going back to the home adding checking the right column for our new article.
Finally run, the following to destroy the resources from this example
cdk destroy
- https://github.com/ksmin23/my-aws-cdk-examples/tree/main/api-gateway/dynamodb-cognito
- https://github.com/cloudcomponents/cdk-constructs/tree/master/packages/cdk-cloudfront-authorization
- https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-browser-credentials-cognito.html
- https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/credentials-explicit.html
- https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html
- https://aws.amazon.com/blogs/compute/building-an-image-searching-solution-with-the-aws-cdk/
- https://haydnjmorris.medium.com/uploading-photos-to-aws-s3-getting-started-with-cognito-and-iam-c96ba5b5496d
- https://blog.bytebytego.com/p/what-is-sso-episode-7
- https://datatracker.ietf.org/doc/html/rfc7519#page-9
- https://www.youtube.com/watch?v=l-6QSEqDJPo
- https://remix.run/docs/en/main/start/quickstart
- https://github.com/xHomu/remix-v2-server/tree/cjs-server.ts
- https://www.color-hex.com/color-palette/30023
- https://help.okta.com/oie/en-us/content/topics/deploymentguides/aws/aws-deployment.htm
- http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0-cd-02.html#3.High-Level%20SAML%20Use%20Cases|outline
- https://jonmeyers.io/blog/create-a-simple-cookie-with-remix/
- https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity/command/GetIdCommand/
- https://youtu.be/996OiexHze0?t=1864
- https://youtu.be/uv-EYcdGUuI?t=104
- https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp.html
- https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html
- http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0-cd-02.html#5.1.2.SP-Initiated%20SSO:%20%20Redirect/POST%20Bindings|outline
- https://community.auth0.com/t/federated-logout-from-oidc-application-auth0-as-saml-sp/81086