I want to control access to a public AWS API, meaning only my website and mobile app can consume this API, otherwise the API will reject the requests.
My approach is to secure the API using AWS Cognito with OAuth2 scopes, see more details here. The website won't need user registration, so I use cognito just to secure the API.
So far, I'm able to go to Hosted UI
under App client settings
in AWS Cognito. Click the link there to login, and get a "code": https://example.com/?code=f27b2de0-1111-1111-1111-11111111. And then follow How to use the code returned from Cognito to get AWS credentials? to get id_token
. And finally send a HTTP request with header key=Authorization
and value=<id_token>
. This works for me. I see that the API rejects requests when no valid token, and returns expected results when valid token is present.
However, I have some questions:
- The token expires in 30 days(I configured it to be so), and my website is a reactJS app, how can the website refresh the token? My website should not care any login here, it just need to send a valid ID Token as the Authorization header to access the API. Users should not see any login page. Should I use Cognito SDK amazon-cognito-identity-js in my react app to fetch the ID token?
- Do I need setup callback url here? All I need is ensure that the API will reject the request if token is missing or invalid, I don't know what's the purpose of having callback url in this case.
Feel free to point out any other mistakes I made here.
Sample code
Here are my CDK code to setup API + Cognito.
import * as CDK from "aws-cdk-lib";
import * as CertificateManager from "aws-cdk-lib/aws-certificatemanager";
import * as Route53 from "aws-cdk-lib/aws-route53";
import * as Route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as ApiGateway from "aws-cdk-lib/aws-apigateway";
import * as ELBv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Construct } from "constructs";
import { StageInfo } from "../config/stage-config";
import * as Cognito from "aws-cdk-lib/aws-cognito";
export interface ApigatewayStackProps extends CDK.StackProps {
readonly packageName: string;
readonly stageInfo: StageInfo;
}
export class ApigatewayStack extends CDK.Stack {
// Prefix for CDK constrcut ID
private readonly constructIdPrefix: string;
private readonly pandaApiCognitoUserPool: Cognito.UserPool;
private readonly domainCertificate: CertificateManager.Certificate;
private readonly apiAuthorizer: ApiGateway.CfnAuthorizer;
private readonly pandaApi: ApiGateway.RestApi;
constructor(scope: Construct, id: string, props: ApigatewayStackProps) {
super(scope, id, props);
this.constructIdPrefix = `${props.packageName}-${props.stageInfo.stageName}`;
const hostedZone: Route53.IHostedZone = Route53.HostedZone.fromLookup(
this,
`${this.constructIdPrefix}-HostedZoneLookup`,
{
domainName: props.stageInfo.domainName,
}
);
this.domainCertificate = new CertificateManager.Certificate(
this,
`${this.constructIdPrefix}-pandaApiCertificate`,
{
domainName: props.stageInfo.domainName,
validation:
CertificateManager.CertificateValidation.fromDns(hostedZone),
}
);
this.pandaApi = new ApiGateway.RestApi(
this,
`${this.constructIdPrefix}-pandaApi`,
{
description: "The centralized API for panda.com",
domainName: {
domainName: props.stageInfo.domainName,
certificate: this.domainCertificate,
//mappingKey: props.pipelineStageInfo.stageName
},
defaultCorsPreflightOptions: {
allowOrigins: ApiGateway.Cors.ALL_ORIGINS,
allowMethods: [...ApiGateway.Cors.DEFAULT_HEADERS],
},
}
);
new Route53.ARecord(this, "AliasRecord", {
zone: hostedZone,
target: Route53.RecordTarget.fromAlias(
new Route53Targets.ApiGateway(this.pandaApi)
),
// or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(domainName)),
});
this.pandaApiCognitoUserPool = new Cognito.UserPool(this, "UserPool", {
userPoolName: `pandaApiUserPool`,
selfSignUpEnabled: false,
});
this.apiAuthorizer = new ApiGateway.CfnAuthorizer(
this,
`${this.constructIdPrefix}-pandaApiAuthorizer`,
{
name: "pandaApiAuthorizer",
type: ApiGateway.AuthorizationType.COGNITO,
identitySource: "method.request.header.Authorization",
restApiId: this.pandaApi.restApiId,
providerArns: [this.pandaApiCognitoUserPool.userPoolArn],
}
);
this.addCognitoAuthentication(props);
}
private addCognitoAuthentication(props: ApigatewayStackProps) {
this.pandaApiCognitoUserPool.addDomain("DomainName", {
cognitoDomain: {
domainPrefix: `panda-api-user-pool-${props.stageInfo.stageName.toLocaleLowerCase()}`,
},
});
this.pandaApiCognitoUserPool.addClient(
`${this.constructIdPrefix}-pandaApiUserPoolClient`,
{
userPoolClientName: `pandaApiUserPoolClient`,
generateSecret: true,
oAuth: {
flows: {
// It's highly recommend to use only the Authorization code grant flow.
// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html
authorizationCodeGrant: true,
},
scopes: [Cognito.OAuthScope.OPENID],
//callbackUrls: [props.stageInfo.domainName + '/callback']
},
authFlows: {
userPassword: true,
},
refreshTokenValidity: CDK.Duration.days(30),
}
);
}
}