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:
Feel free to point out any other mistakes I made here.
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),
}
);
}
}
Cognito is for logging your users in and providing you an access token
and id token
as a result. Your users must log in. If they don't log in then you can't use cognito to secure you API. Further if you don't require log in, then anyone in the world can call your API.
There is no way to ensure only your website and your mobile app can talk to your API, this isn't how APIs work. APIs are available to the whole world, they are all public, you can't block requests to them, unless your users individually authenticate.
The reason for this is that any one can spoof your website or mobile application, and because of that no security mechanisms exist that would enable restricting access.
So either your API is world-writable/readable or you require your users to log in. The good news is there are tons of ways to easily do this, now that logs of AuthN providers support webauthn/fido2/passkeys out of the box.
you can probably google around for a list of providers that offer exactly what you need, but a starter list is something like this one on how to pick the best auth provider.