Day 009 - 100DaysAWSIaCDevopsChallenge
Recently, on day 008 of my 100 Days of Code challenge, I created an infrastructure to deploy a REST API behind an API Gateway and an Angular app to consume the exposed APIs. Today, I am going to secure some of those APIs with AWS Cognito. To access these APIs, the user will need to provide an ACCESS
or ID
token in the header of every requests made.
To achieve this, we will follow the steps below:
- Set Up AWS Cognito User Pool: Create an User Pool to handler users, and then create the App Client for authentication needs.
- Create Lambda functions for authentication process: Develop the necessary Lambda functions to support the authentication process.
- Create Api Gateway Authorizer: Set up an API Gateway authorizer of type Cognito.
- Update the Rest Api Methods: Secure the methods that need protection by applying the Cognito authorizer.
- Updated the Angular App with authentication proccess: Implement the authentication process within the Angular app.
- Re-deploy the app as S3 static website
Prerequises
- AWS Cloud Developement Kit (CDK)
- Cognito, Secrets Manager, Lambda, API Gateway, IAM
- Typescript, Angular, esbuild
Infrastructure Diagram
Set Up AWS Cognito User Pool
I chose to create my own CDK construct to achieve this. In my construct, I first create the User Pool
and then associate an App Client
with it. Before these two resources are created, I will store the raw app client secret in an AWS Secrets Manager
resource for added security, and I will retrieve it directly inside the Lambda by key.
Cogniton Construct
import { Construct } from 'constructs'
import { aws_cognito as cognito, aws_secretsmanager as sm, Duration, RemovalPolicy, StackProps } from 'aws-cdk-lib'
interface CognitoProps extends StackProps {
verificationEmailUrl: string,
fromEmail: string
}
export class Cognito extends Construct {
private readonly _defaultAppClientId: string
private readonly _secretValueArn: string
private readonly _userPoolArn: string
private readonly _userPoolId: string
private readonly _userPool: cognito.IUserPool
constructor(scope: Construct, id: string, props: CognitoProps) {
super(scope, id)
const stdRequired: cognito.StandardAttribute = {
required: true
}
const userPool = new cognito.UserPool(this, 'CognitoUserPoolResource', {
removalPolicy: RemovalPolicy.DESTROY,
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
userPoolName: 'awschallenge-userpool-2',
selfSignUpEnabled: true,
userInvitation: {
emailSubject: 'An account has been created for you',
emailBody: 'Your Cognito account has been created by an admin. Here is your username: {username} and your temporary password: {####}'
},
signInCaseSensitive: false,
signInAliases: {
email: true,
username: true
},autoVerify: {
email: true,
phone: true
},keepOriginal: {
email: true
},standardAttributes: {
email: { ...stdRequired, mutable: true }
},
customAttributes: {
'domain': new cognito.StringAttribute(),
'created_at': new cognito.StringAttribute(),
'last_updated_at': new cognito.StringAttribute(),
'verified_at': new cognito.StringAttribute(),
'first_name': new cognito.StringAttribute(),
'last_name': new cognito.StringAttribute()
},
passwordPolicy: {
tempPasswordValidity: Duration.days(14),
minLength: 6,
requireDigits: true,
requireUppercase: true,
requireLowercase: true,
requireSymbols: true
},
email: cognito.UserPoolEmail.withCognito(props.fromEmail)
})
userPool.addTrigger(cognito.UserPoolOperation.CUSTOM_MESSAGE, this.customEmailMessageLambda())
const userPoolClient = userPool.addClient('todo-app-client', {
authFlows: {
userPassword: true,
adminUserPassword: true,
custom: true,
userSrp: true
},
disableOAuth: true,
preventUserExistenceErrors: true,
generateSecret: true,
userPoolClientName: 'Todo-App-Client',
accessTokenValidity: Duration.minutes(30),
idTokenValidity: Duration.minutes(30),
refreshTokenValidity: Duration.days(30)
})
const clientSecret = new sm.Secret(this, 'SecretManagerResource', {
secretName: `${userPool.userPoolId}/${userPoolClient.userPoolClientId}`,
generateSecretString: {
secretStringTemplate: JSON.stringify({
appClientSecret: userPoolClient.userPoolClientSecret.unsafeUnwrap()
}),
generateStringKey: `${userPoolClient.userPoolClientName}-AppSecret`
}
})
this._defaultAppClientId = userPoolClient.userPoolClientId
this._secretValueArn = clientSecret.secretArn
this._userPoolArn = userPool.userPoolArn
this._userPoolId = userPool.userPoolId
this._userPool = userPool
}
get defaultAppClientId() {
return this._defaultAppClientId
}
get secretValueArn() {
return this._secretValueArn
}
get userPoolArn() {
return this._userPoolArn
}
get userPoolId() {
return this._userPoolId
}
get userPool() {
return this._userPool
}
customEmailMessageLambda = () => {
return new LambdaFunction(this, 'CustomVerificationMessage', {
functionName: 'CustomMessageFunction',
entryFunction: './apps/functions/auth/custom-confirm-message.function.ts',
env: {
CONFIRM_URL: this.props.verificationEmailUrl
},
logged: true
}).resource
}
}
The Lambda construct
import * as cdk from 'constructs'
import { Construct } from 'constructs'
import { Aws, aws_apigateway, aws_iam as iam, aws_lambda as lambda, Duration, StackProps } from 'aws-cdk-lib'
import { NodejsFunction, SourceMapMode } from 'aws-cdk-lib/aws-lambda-nodejs'
import { generateResourceID } from './utils'
interface Props {
entryFunction: string;
functionName: string;
env?: Record<string, string>;
layersArns?: string[];
externalDeps?: string[];
logged?: boolean;
memory?: number;
role?: {
name?: string,
inlinePolicies?: Record<string, iam.PolicyDocument>
};
}
export class LambdaFunction extends cdk.Construct {
private readonly _functionArn: string
private readonly _resource: NodejsFunction
constructor(scope: Construct, id: string, private param: Props) {
super(scope, id)
const policies: Record<string, iam.PolicyDocument> = param.role?.inlinePolicies ?? {}
if (param.logged) {
policies['logging'] = new iam.PolicyDocument({
assignSids: true,
statements: [
new iam.PolicyStatement({
actions: [
'logs:CreateLogGroup',
'logs:PutLogEvents',
'logs:CreateLogStream'
],
effect: iam.Effect.ALLOW,
resources: ['*']
})
]
})
}
const lamnbdaRole = this.createLambdaRole(
param.role?.name ?? `Role${generateResourceID()}`,
param,
policies
)
const func = new NodejsFunction(this, `Lambda${generateResourceID()}Resource`, {
entry: param.entryFunction,
handler: 'index.handler',
timeout: Duration.seconds(10),
functionName: param.functionName,
environment: param.env,
runtime: lambda.Runtime.NODEJS_20_X,
memorySize: param.memory ?? 128,
role: lamnbdaRole,
bundling: {
externalModules: [
...(param.externalDeps || []),
'utils'
],
sourceMap: true,
sourceMapMode: SourceMapMode.BOTH
},
layers: [
...((param.layersArns || [])
.filter(value => !!value && value.length)
.map(value => lambda.LayerVersion.fromLayerVersionArn(this, `LayerVersion${generateResourceID()}Resource`, value)))
]
})
this._functionArn = func.functionArn
this._resource = func
}
get functionArn() {
return this._functionArn
}
get resource() {
return this._resource
}
grantApi = (restApi: aws_apigateway.RestApi) => {
this._resource.addPermission(`TodoAppLambdaPermissionResource-${randomUUID()}`, {
action: 'lambda:InvokeFunction',
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceAccount: restApi.env?.account,
sourceArn: `arn:${Aws.PARTITION}:execute-api:${restApi.env?.region}:${restApi.env?.account}:${restApi.restApiId}/*/*/*`
})
}
private createLambdaRole(roleName: string, props: StackProps, inlinePolicies?: Record<string, iam.PolicyDocument>): iam.Role {
return new iam.Role(this, `${roleName}Resource`, {
roleName: roleName,
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com', {
region: props.env?.region
}),
path: '/',
inlinePolicies: {
...inlinePolicies
}
})
}
}
The important things here to know is:
-
accountRecovery
Specifies the method for users to recover their account if they lose their password. In our case, it's set to email only, so the recovery link will be sent to the user via email provided by him. -
selfSignUpEnabled
Give the habillity to the users (unknown user) to use theSign-Up
functionality to create their accounts. userInvitation
The message template (subject and body) sent to the user when their account is created by an admin. In the emailBody,{username}
and{####}
will be replaced by the user's username and the verification code, respectively.signInAliases
A list of methods by which a user can register or sign in to a user pool. It allows either username with aliases or sign-in with email, phone, or both. In our case, the user can sign in using either their email or username.customAttributes
Additional attributes for the user. Note that each attribute here will be represented in the Cognito schema ascustom:<ATTR_NAME>
, so there's no need to prefix these attributes with custom.email
The email settings for the User Pool. There are two options:Cognito
orSES
. Since we chose the Cognito option, we provide the no-reply address (sender name )
In the code above, we added a Lambda Trigger of type CUSTOM_MESSAGE
to customize the email content for a more appealing appearance. This trigger activates when the verification code email is sent to the user after sign-up.
userPool.addTrigger(
cognito.UserPoolOperation.CUSTOM_MESSAGE,
this.customEmailMessageLambda()
)
The rendering will be hardcoded in the provided Lambda and will look like this.
You can find the entire source code for the function here custom-confirm-message.function.tsβ
Now let's talk about the app client. The important points to note are:
-
disableOAuth
set tofalse
to desable the OAuth interaction. -
generateSecret
set totrue
to generate the app client secret. -
authFlows
Specifies the set of OAuth authentication flows to enable on the client
Note that for security purposes, it is recommended to retrieve the secret at Lambda runtime logic to avoid exposing the secret in the CloudFormation template. For this, pass the secret ARN as an environment variable to the lambda function instead of passing the secret value directly.
As you can see, we also store the app client secret using AWS Secrets Manager with the following snippet of code
const clientSecret = new sm.Secret(this, 'SecretManagerResource', {
secretName: `${userPool.userPoolId}/${userPoolClient.userPoolClientId}`,
generateSecretString: {
secretStringTemplate: JSON.stringify({
appClientSecret: userPoolClient.userPoolClientSecret.unsafeUnwrap()
}),
generateStringKey: `${userPoolClient.userPoolClientName}-AppSecret`
}
})
Create Lambda functions for authentication process
In this section, I will create three (03) Lambda functions for Sign-In, Sign-Up, and Confirm Email.
Lambda function for user Sign-Up
apps/functions/auth/register.function.ts
import { APIGatewayProxyEvent, Context, Handler } from 'aws-lambda'
import { LambdaResponse } from '../../src/infra/dto/lambda.response'
import {
CognitoIdentityProviderClient,
SignUpCommand,
SignUpCommandOutput
} from '@aws-sdk/client-cognito-identity-provider'
import moment from 'moment'
import { computeSecretHash, DATETIME_FORMAT, responseError, responseOk, toPayload } from 'utils'
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
import { SecretsManagerRepository } from '../../src/infra/storage/secrets/secrets-manager.repository'
const client = new CognitoIdentityProviderClient({})
const smClient = new SecretsManagerClient({
region: process.env.REGION || 'us-east-1'
})
const CLIENT_ID = process.env.APP_CLIENT_ID
const SECRET_VALUE_ARN = process.env.SECRET_VALUE_ARN
export const handler: Handler = async (event: APIGatewayProxyEvent, context: Context) => {
const payload = toPayload(event)
let responseHandler: LambdaResponse
if (!validatePayload(payload)) {
responseHandler = responseError('some Input(s) are invalid', 403)
} else {
const attributes = [{
Name: 'email',
Value: payload.email
}, {
Name: 'custom:domain',
Value: 'nivekaa.com'
}, {
Name: 'custom:first_name',
Value: payload.firstName
}, {
Name: 'family_name',
Value: payload.lastName
}, {
Name: 'given_name',
Value: payload.firstName
}, {
Name: 'custom:last_name',
Value: payload.lastName
}, {
Name: 'custom:created_at',
Value: moment().format(DATETIME_FORMAT)
}, {
Name: 'custom:last_updated_at',
Value: moment().format(DATETIME_FORMAT)
}, {
Name: 'name',
Value: (!!payload.lastName || !!payload.firstName) ? `${payload.firstName} ${payload.lastName}`.trim() : null
}]
let messageError = null
try {
const clientSecret = await new SecretsManagerRepository(smClient)
.getCognitoAppClientSecretValue(SECRET_VALUE_ARN!)
const command = new SignUpCommand({
ClientId: CLIENT_ID,
Username: payload.username,
Password: payload.password,
SecretHash: computeSecretHash(CLIENT_ID, clientSecret, payload.username),
UserAttributes: attributes,
UserContextData: {
IpAddress: event.requestContext?.identity?.sourceIp
}
})
const response: SignUpCommandOutput = await client.send(command)
if (response.$metadata.httpStatusCode !== 200) {
messageError = `Something wrong: StatusCode=${response.$metadata.httpStatusCode}`
}
} catch (e) {
console.error(e)
messageError = e.message
}
if (messageError !== null) {
return responseError(messageError)
} else {
return responseOk({}, 'User successfully created')
}
}
return responseHandler
}
const validatePayload = (body: any): boolean => {
const email = body.email
const password = body.password
const username = body.username
const areNotNull = [email, password, username].every(value => !!value)
const isEmailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
const isValidPassword = /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*\W)(?!.* ).{8,16}$/.test(password)
return areNotNull && isEmailValid && isValidPassword
}
Considering that the SECRET_VALUE_ARN
and APP_CLIENT_ID
were passed to the Lambda resource during the infrastructure construction, remember that in the previous section, I stored the user pool app client secret in Secrets Manager. Below is the code to retrieve it:
// app/src/infra/storage/secrets/secrets-manager.repository.ts
import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
export class SecretsManagerRepository {
constructor(private client: SecretsManagerClient) {
}
getCognitoAppClientSecretValue = async (secretArn: string): Promise<string> => {
const command = new GetSecretValueCommand({
SecretId: secretArn
})
try {
const response = await this.client.send(command)
if (response.$metadata.httpStatusCode === 200) {
const json = JSON.parse(response.SecretString!)
return json.appClientSecret + ''
}
} catch (e) {
console.error(e)
}
throw new Error('Technical error. Please Contact the admin or try again later')
}
}
And below is the content of function to compute the SecretHash parameter
import { createHmac } from 'node:crypto'
....
export const computeSecretHash = (clientId: string, clientSecret: string, username: string): string => {
const hasher = createHmac('sha256', clientSecret + '')
hasher.update(`${username}${clientId}`)
return hasher.digest('base64')
}
For more information about the client-secret hash value see aws docsβ
Lambda function for user Sign-In
apps/functions/auth/login.function.ts
import { APIGatewayProxyEvent, Context, Handler } from 'aws-lambda'
import { computeSecretHash, responseError, toPayload } from 'utils'
import {
AuthFlowType,
CognitoIdentityProviderClient,
InitiateAuthCommand
} from '@aws-sdk/client-cognito-identity-provider'
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
import { SecretsManagerRepository } from '../../src/infra/storage/secrets/secrets-manager.repository'
const client = new CognitoIdentityProviderClient({
region: process.env.REGION || 'us-east-1'
})
const smClient = new SecretsManagerClient({
region: process.env.REGION || 'us-east-1'
})
const CLIENT_ID = process.env.APP_CLIENT_ID
const SECRET_VALUE_ARN = process.env.SECRET_VALUE_ARN
export const handler: Handler = async (event: APIGatewayProxyEvent, context: Context) => {
const payload = toPayload(event)
if (!validatePayload(payload)) {
return responseError('Invalid input(s).', 403)
}
try {
const clientSecret = await new SecretsManagerRepository(smClient)
.getCognitoAppClientSecretValue(SECRET_VALUE_ARN!)
const command = new InitiateAuthCommand({
ClientId: CLIENT_ID,
AuthFlow: AuthFlowType.USER_PASSWORD_AUTH,
AuthParameters: {
SECRET_HASH: computeSecretHash(CLIENT_ID!, clientSecret, payload.username),
USERNAME: payload.username,
PASSWORD: payload.password
},
UserContextData: {
IpAddress: event.requestContext?.identity?.sourceIp
}
})
const response = await client.send(command)
if (response.$metadata.httpStatusCode !== 200) {
return responseError(`Something wrong!! Status=${response.$metadata.httpStatusCode}`)
}
return {
isBase64Encoded: false,
statusCode: 200,
headers: {
Authorization: `Bearer ${response.AuthenticationResult?.IdToken}`
},
body: JSON.stringify({
message: 'user logged!!',
idToken: response.AuthenticationResult?.IdToken,
accessToken: response.AuthenticationResult?.AccessToken,
tokenType: response.AuthenticationResult?.TokenType,
expiredIn: response.AuthenticationResult?.ExpiresIn,
refreshToken: response.AuthenticationResult?.RefreshToken
})
}
} catch (e) {
console.error(e)
return responseError(e.message)
}
}
const validatePayload = (payload: any): boolean => {
return (payload.hasOwnProperty('username') && payload.hasOwnProperty('password'))
&& [payload.username, payload.password].every(value => !!value)
}
This function initiates the authentication process by generating the Id and Access tokens that will be used by external systems to access API resources.
Lambda function for Confirm Email
This function will be called when the user clicks on theActivate Account
button inside the verification email. We need the code
(generated by Cognito during the sign-up process) and username
for this process.
import { APIGatewayProxyEvent, Context, Handler } from 'aws-lambda'
import { CognitoIdentityProviderClient, ConfirmSignUpCommand } from '@aws-sdk/client-cognito-identity-provider'
import { computeSecretHash, responseError, responseOk, toPayload } from 'utils'
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
import { SecretsManagerRepository } from '../../src/infra/storage/secrets/secrets-manager.repository'
const client = new CognitoIdentityProviderClient({})
const smClient = new SecretsManagerClient({
region: process.env.REGION || 'us-east-1'
})
const CLIENT_ID = process.env.APP_CLIENT_ID
const SECRET_VALUE_ARN = process.env.SECRET_VALUE_ARN
const DOMAIN = process.env.DOMAIN
export const handler: Handler = async (event: APIGatewayProxyEvent, context: Context) => {
const payload = toPayload(event)
if (!validatePayload(payload)) {
return responseError('Invalid input(s)', 403)
}
if (payload.hasOwnProperty('email') && payload.email && DOMAIN !== payload.email.split('@')[1]) {
return responseError('The email is wrong.')
} else {
try {
const clientSecret = await new SecretsManagerRepository(smClient)
.getCognitoAppClientSecretValue(SECRET_VALUE_ARN!)
const cmd = new ConfirmSignUpCommand({
ConfirmationCode: payload.code + '',
ClientId: CLIENT_ID,
Username: payload.username,
SecretHash: computeSecretHash(CLIENT_ID!, clientSecret, payload.username)
})
const response = await client.send(cmd)
console.log(response)
if (response.$metadata.httpStatusCode === 200) {
return responseOk('Email confirmed')
}
return responseError('Something unexpected wrong!', response.$metadata.httpStatusCode)
} catch (e) {
console.error(e)
return responseError(e.message)
}
}
}
const validatePayload = (payload: any): boolean => {
return (payload.hasOwnProperty('username') && payload.hasOwnProperty('code'))
&& [payload.username, payload.code].every(value => !!value)
}
Add methods for authentication process
Now that all the functions for the authentication process are ready, let's add methods for them to the API Gateway.
stacks/cdk-stack.ts
const cors: api.CorsOptions = {...}
const layerArn = ...
...
const rootResource = restApi.root.addResource('todo-app-api')
const authRoot = restApi.root.addResource('auth')
// Create the authentication endpoints
const loginResource = authRoot.addResource('login', {
defaultCorsPreflightOptions: cors
})
const registerResource = authRoot.addResource('register', {
defaultCorsPreflightOptions: cors
})
const confirmResource = authRoot.addResource('confirm-email', {
defaultCorsPreflightOptions: cors
})
// Add authentication methods with each lambda integration
const cognitoInlinePolicy = {
cognito: new iam.PolicyDocument({
assignSids: true,
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'cognito-idp:ConfirmDevice',
'cognito-idp:ChangePassword',
'cognito-idp:ConfirmForgotPassword',
'cognito-idp:ChangePassword',
'cognito-idp:ConfirmSignUp',
'cognito-idp:ForgotPassword',
'cognito-idp:GetUser',
'cognito-idp:GetUserAttributeVerificationCode',
'cognito-idp:ListUsers',
'cognito-idp:SignUp',
'cognito-idp:UpdateUserAttributes'
],
resources: [
cognito.userPoolArn
]
})
]
}),
secret: new iam.PolicyDocument({
assignSids: true,
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue'],
resources: [cognito.secretArn]
})
]
})
}
const loginFunction = new LambdaFunction(this, 'LoginFunction', {
functionName: 'Auth-Cognito-Login-NodejsFunction',
entryFunction: './apps/functions/auth/login.function.ts',
env: {
USER_POOL_ID: cognito.userPooId,
SECRET_VALUE_ARN: cognito.secretArn,
APP_CLIENT_ID: cognito.clientId,
REGION: this.props.env?.region!
},
logged: true,
role: { inlinePolicies: cognitoInlinePolicy },
layersArns: [layerArn],
externalDeps: Object.keys(dependencies)
})
const registerFunction = new LambdaFunction(this, 'RegistrationFunction', {
functionName: 'Auth-Cognito-Register-NodejsFunction',
entryFunction: './apps/functions/auth/register.function.ts',
env: {
USER_POOL_ID: cognito.userPooId,
SECRET_VALUE_ARN: cognito.secretArn,
APP_CLIENT_ID: cognito.clientId,
REGION: this.props.env?.region!
},
role: {
name: 'AuthRegistrationRole',
inlinePolicies: cognitoInlinePolicy
},
layersArns: [layerArn],
externalDeps: Object.keys(dependencies)
})
const confirmUserFunction = new LambdaFunction(this, 'ConfirmUserFunction', {
functionName: 'Auth-Cognito-ConfirmUser-NodejsFunction',
entryFunction: './apps/functions/auth/confirm-user.function.ts',
env: {
USER_POOL_ID: cognito.userPooId,
SECRET_VALUE_ARN: cognito.secretArn,
APP_CLIENT_ID: cognito.clientId,
DOMAIN: this.props.cognito?.domain!,
REGION: this.props.env?.region!
},
logged: true,
role: {
name: 'AuthConfirmRole',
inlinePolicies: cognitoInlinePolicy
},
layersArns: [layerArn],
externalDeps: Object.keys(dependencies)
})
// Allow each lambda to be invoked by API Gateway
Array.of(loginFunction, registerFunction, confirmUserFunction)
.forEach(func => {
func.resource.addPermission(`APIGW-Permission-${randomUUID()}`, {
principal: new iam.ServicePrincipal('apigateway.amazonaws.com'),
sourceArn: `arn:${Aws.PARTITION}:execute-api:${this.props.env?.region}:${this.props.env?.account}:${restApi.restApiId}/*/*/*`,
sourceAccount: this.props.env?.account,
scope: this,
action: 'lambda:InvokeFunction'
})
})
const loginMethod = auth.loginResource.addMethod(MediaType.POST, new api.LambdaIntegration(loginFunction.resource, {
proxy: true
}), {
authorizationType: api.AuthorizationType.NONE,
methodResponses
})
const registerMethod = auth.registerResource.addMethod(MediaType.POST, new api.LambdaIntegration(registerFunction.resource, {
proxy: true
}), {
authorizationType: api.AuthorizationType.NONE,
methodResponses
})
const confirmUserMethod = auth.confirmResource.addMethod(MediaType.POST, new api.LambdaIntegration(confirmUserFunction.resource, {
proxy: true
}), {
methodResponses,
authorizationType: api.AuthorizationType.NONE
})
β οΈβ οΈ Pay attention to the Lambdas set up above. Since they call Cognito and Secrets Manager, it is necessary to grant them the appropriate permissions to perform their operations without causing issues. Additionally, youβll notice that API Gateway is granted permission to invoke all of these Lambda functions.
Create Api Gateway Authorizer
Before creating the Cognito Authorizer, it is imperative to instantiate our construct created in the User Pool section, because the authorizer needs its ARN.
const cognitoUserPool = new Cognito(this, 'CustomCognitoResource', {
verificationEmailUrl: 'https://localhost:4200/auth/confirm-email',
fromEmail: props.cognito?.verificationFromEmail!
})
Now that the user pool is constructed, we can create the Cognito authorizer and attach it directly to the User Pool we previously created.
const cognitoAuthorizer = new api.CognitoUserPoolsAuthorizer(this, 'AuthorizerResource', {
authorizerName: 'todo-app-authorizer',
cognitoUserPools: [cognitoUserPool.userPool],
identitySource: api.IdentitySource.header('authorization')
})
api.IdentitySource.header('authorization') will return the string 'method.request.header.Authorization'
Update the Rest Api Methods
In my last article (Day 008β), I had created insecure APIs. Now, let's update these resources by securing them with the Cognito authorizer that we previously created.
const getTodoListsMethod = getTodoListsResource.addMethod(MediaType.GET, new api.LambdaIntegration(lambdaFunction.resource, {
connectionType: ConnectionType.INTERNET,
proxy: true
}), {
authorizationType: api.AuthorizationType.COGNITO,
authorizer: cognitoAuthorizer,
methodResponses
})
const createTodoListMethod = createTodoListResource.addMethod(MediaType.POST, new api.LambdaIntegration(lambdaFunction.resource, {
proxy: true,
connectionType: ConnectionType.INTERNET
}), {
authorizer: cognitoAuthorizer,
authorizationType: api.AuthorizationType.COGNITO,
methodResponses
})
const updateTodoListMethod = updateTodoListResource.addMethod(MediaType.PUT, new api.LambdaIntegration(lambdaFunction.resource, {
proxy: true,
connectionType: api.ConnectionType.INTERNET
}), {
authorizer: cognitoAuthorizer,
authorizationType: api.AuthorizationType.COGNITO,
methodResponses
})
const deleteTodoListMethod = deleteTodoListResource.addMethod(MediaType.DELETE, new api.LambdaIntegration(lambdaFunction.resource, {
connectionType: api.ConnectionType.INTERNET,
proxy: true
}), {
authorizer: cognitoAuthorizer,
authorizationType: api.AuthorizationType.COGNITO,
methodResponses
})
Now let's test one of these APIs to verify that the security integration is working correctly.
I received a 401 Unauthorized
error, indicating that I need to provide either the Access or ID token.
I copied the idToken, included it in the header of my request, and tried again. This time, I received:
Updated the Angular App with authentication proccess
The app integration involves passing the IdToken
generated during the sign-in process in the header of each request, formatted as Authorization: Bearer <ID_TOKEN>
export const authorizerInterceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
const tokenAuth = inject(MEMORY_TOKEN_REPOSITORY)
const url = req.url
if (!url.includes('/auth/') || url.endsWith('/auth/details')) {
if (tokenAuth.hasToken()) {
const reReq = req.clone({
setHeaders: {
Authorization: `Bearer ${tokenAuth.getToken()}`
}
})
return next(reReq)
}
}
return next(req)
}
You can find the complete code for the Angular app hereβ
Deploy the entire infrastructure
git clone https://github.com/nivekalara237/100DaysTerraformAWSDevops.git
export STAGE_NAME=dev # needed by api gateway staging
cd 100DaysTerraformAWSDevops/day_009
cdk deploy --profile cdk-user --all
Re-deploy app as S3 static website
To deploy the Angular app to the S3 bucket as a website, follow the instructions in my previous article ππ½ππ½ Deploying a REST API and Angular Frontend Using AWS CDK, S3, and API Gateway
β οΈβ οΈ Make sure to update the API Gateway URL in the src/environment.ts file with the correct API Gateway endpoint before building and deploying the application.
export const environment = {
production: true,
apiUrl: 'https://{{API_GATEWAY_ID}}.execute-api.us-east-1.amazonaws.com/{{STAGE_NAME}}/'
}
__
π₯³β¨
We have reached the end of the article.
Thank you so much π
Your can find the full source code on GitHub Repo
Top comments (0)