Navigation

I got ID! Adding Authorisation to our App!

Why not show off a bit of my fondness towards Pearl Jam?

In our previous chapters of this series we have:

That's right! Today we will add an additional layer of security to our Angular application by integrating an AWS Lambda Authorizer as an authentication and authorization method, using either AWS Cognito or Okta as a third-party provider.

Create a Lambda Authorizer function

The first step in creating a Lambda Authorizer is to create the Lambda function itself. The function will handle the authentication and authorization process by validating the incoming token and determining if the user is authorized to access the resources.

Here's an example of a simple Lambda Authorizer function written in Node.js that uses AWS Cognito as the third-party provider:

const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider();
exports.handler = async (event) => {
    // Extract the token from the request
    const token = event.authorizationToken;
    
    // Verify the token using the Cognito provider
    const params = {
        AccessToken: token
    };
    const data = await cognito.getUser(params).promise();
    
    // Determine if the user is authorized
    const user = data.UserAttributes;
    const isAuthorized = user.some(attr => attr.Name === 'custom:groups' && attr.Value === 'admin');
    
    // Return the policy statement
    if (isAuthorized) {
        return {
            principalId: user.find(attr => attr.Name === 'sub').Value,
            policyDocument: {
                Version: '2012-10-17',
                Statement: [
                    {
                        Action: 'execute-api:Invoke',
                        Effect: 'Allow',
                        Resource: event.methodArn
                    }
                ]
            }
        };
    } else {
        return {
            principalId: 'user',
            policyDocument: {
                Version: '2012-10-17',
                Statement: [
                    {
                        Action: 'execute-api:Invoke',
                        Effect: 'Deny',
                        Resource: event.methodArn
                    }
                ]
            }
        };
    }
};

If you are using Okta as the provider, you should use Okta's SDK and API to verify the token and determine if the user is authorized.

const OktaJwtVerifier = require('@okta/jwt-verifier');
const oktaJwtVerifier = new OktaJwtVerifier({
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  clientId: '{clientId}'
});
exports.handler = async (event) => {
    // Extract the token from the request
    const token = event.authorizationToken;
    
    // Verify the token using the Okta provider
    try {
        const jwt = await oktaJwtVerifier.verifyAccessToken(token);
        // Verify that the user has the required scope
        if (jwt.claims.scp.split(" ").includes("admin")) {
            return {
                principalId: jwt.claims.sub,
                policyDocument: {
                    Version: '2012-10-17',
                    Statement: [
                        {
                            Action: 'execute-api:Invoke',
                            Effect: 'Allow',
                            Resource: event.methodArn
                        }
                    ]
                }
            };
        }
    } catch (err) {
        console.log(err);
    }
    // Return a policy statement denying access
    return {
        principalId: 'user',
        policyDocument: {
            Version: '2012-10-17',
            Statement: [
                {
                    Action: 'execute-api:Invoke',
                    Effect: 'Deny',
                    Resource: event.methodArn
                }
            ]
        }
    };
};

This example uses the @okta/jwt-verifier package to verify the access token and check for the required scope(in this case "admin").

Create the Lambda Authorizer on API Gateway

Once the Lambda Authorizer function is created, it needs to be added as an Authorizer on the API Gateway. This can be done using the AWS Management Console or using Terraform. 

resource "aws_api_gateway_authorizer" "authorizer_name" {
  name = "your-authorizer-name"
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  type = "TOKEN"
  identity_source = "method.request.header.Authorization"
  authorizer_uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${aws_lambda_function.authorizer_name.arn}/invocations"
}

The above code creates an Authorizer on the API Gateway and associates it with the Lambda function that we created at the start.

Update the Angular application

Once the Lambda Authorizer is set up, the Angular application needs to be updated to send the token in the Authorization header with each request. This can be done by adding an interceptor that adds the token to the headers before sending the request.

Here's an example of an Angular interceptor that adds the token to the headers:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { TokenService } from './token.service';
import { Observable } from 'rxjs';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
    constructor(private tokenService: TokenService) {}
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        request = request.clone({
            setHeaders: {
                Authorization: `Bearer ${this.tokenService.getToken()}`
            }
        });
        return next.handle(request);
    }
}

This example assumes that you have a TokenService that retrieves the token from storage.

You will also need to register this interceptor in the app.module.ts file.

import { TokenInterceptor } from './token.interceptor';
providers:
...
@NgModule({
  ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true
    }
  ],
  ...
})
export class AppModule { }

This registers the interceptor in the AppModule, ensuring that the token is added to the headers of all HTTP requests made by the application.

Deploy the Terraform Configuration

After updating the Angular application, the Terraform configuration can be deployed to create and configure the necessary resources on AWS.
$ terraform init
$ terraform apply

Verify the Deployment

Now we can test to make sure that the Lambda Authorizer has been set up correctly, you can test the API Gateway endpoint using a tool like Postman and check the logs of the Lambda function to ensure that it is being invoked correctly.

Wrap Up!

By now you should be starting to see how we are building up this application and how you can start to implement your own logic into this magical paradigm. By using a Lambda Authorizer and integrating with a third-party provider like AWS Cognito, Okta or even many others! you can add an additional layer of security to your Angular application.