Skip to content

Commit

Permalink
807: CDK auth stack (AWS Cognito) (#818)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswilty committed Mar 19, 2024
1 parent 5d72051 commit c245172
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 8 deletions.
1 change: 1 addition & 0 deletions cloud/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.js
*.d.ts
node_modules
.env

# CDK asset staging directory
.cdk.staging
Expand Down
33 changes: 33 additions & 0 deletions cloud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,36 @@ The `cdk.json` file tells the CDK Toolkit how to execute your app.
_Caution! This will overwrite existing resources, so ensure the team are notified before running this manually._

Note that once the CDK Pipeline is in place, DEV deployment will happen automatically on merging into dev branch.

---

## First-time admin tasks

If you are setting up the CDK project for the first time, there are a few setup tasks you must complete.

### Bootstrapping the CDK using a Developer Policy

In order to deploy AWS resources to a remote environment using CDK, you must first
[bootstrap the CDK](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). For this project, as per
[CDK guidelines](https://aws.amazon.com/blogs/devops/secure-cdk-deployments-with-iam-permission-boundaries/), we use a
lightweight permissions boundary to restrict permissions, to prevent creation of new users or roles with elevated
permissions. See `cdk-developer-policy.yaml` for details.

Create the permissions boundary CloudFormation stack:

```
aws cloudformation create-stack \
--stack-name CDKDeveloperPolicy \
--template-body file://cdk-developer-policy.yaml \
--capabilities CAPABILITY_NAMED_IAM
```

Then bootstrap the CDK:

```
# install dependencies if not already done
npm install
# run the bootstrap command
npx cdk bootstrap --custom-permissions-boundary cdk-developer-policy
```
11 changes: 11 additions & 0 deletions cloud/bin/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ const tags = {
const generateStackName = stackName(app);
const generateDescription = resourceDescription(app);

// Don't need this stack, yet... Or ever? Will ask Pete C.
/*
const authStack = new AuthStack(app, generateStackName('auth'), {
tags,
description: generateDescription('Auth stack'),
});
*/

new ApiStack(app, generateStackName('api'), {
tags,
description: generateDescription('API stack'),
// userPool: authStack.userPool,
// userPoolClient: authStack.userPoolClient,
// userPoolDomain: authStack.userPoolDomain,
});
2 changes: 1 addition & 1 deletion cloud/cdk.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"app": "npx ts-node --prefer-ts-exts bin/cloud.ts",
"app": "npx ts-node --prefer-ts-exts -r dotenv/config bin/cloud.ts",
"watch": {
"include": ["**"],
"exclude": [
Expand Down
40 changes: 33 additions & 7 deletions cloud/lib/api-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Stack, StackProps } from 'aws-cdk-lib/core';
//import { UserPool, UserPoolClient, UserPoolDomain } from 'aws-cdk-lib/aws-cognito';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import {
Expand All @@ -7,18 +7,26 @@ import {
Secret as EnvSecret,
} from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
//import { ListenerAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
//import { AuthenticateCognitoAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { Stack, StackProps } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { join } from 'node:path';

import { resourceName, stageName } from './resourceNamingUtils';
import { resourceName } from './resourceNamingUtils';

export class ApiStack extends Stack {
stage: string;
type ApiStackProps = StackProps & {
// userPool: UserPool;
// userPoolClient: UserPoolClient;
// userPoolDomain: UserPoolDomain;
};

constructor(scope: Construct, id: string, props: StackProps) {
export class ApiStack extends Stack {
constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
this.stage = stageName(scope);
// TODO Enable cognito auth
//const { userPool, userPoolClient, userPoolDomain } = props;

const generateResourceName = resourceName(scope);

Expand All @@ -45,6 +53,7 @@ export class ApiStack extends Stack {
// Create a load-balanced Fargate service and make it public
const containerPort = 3001;
const serviceName = generateResourceName('fargate');
const loadBalancerName = generateResourceName('alb');
const fargateService = new ApplicationLoadBalancedFargateService(
this,
serviceName,
Expand Down Expand Up @@ -72,10 +81,27 @@ export class ApiStack extends Stack {
},
},
memoryLimitMiB: 512, // Default is 512
loadBalancerName: generateResourceName('elb'),
loadBalancerName,
publicLoadBalancer: true, // Default is true
}
);

// Hook up Cognito to load balancer
// https://stackoverflow.com/q/71124324
// TODO This needs HTTPS and a Route53 domain, so in meantime try VPCLink:
// https://repost.aws/knowledge-center/api-gateway-alb-integration
/*
const authActionName = generateResourceName('alb-auth');
fargateService.listener.addAction(authActionName, {
action: new AuthenticateCognitoAction({
userPool,
userPoolClient,
userPoolDomain,
next: ListenerAction.forward([fargateService.targetGroup]),
}),
});
*/

fargateService.targetGroup.configureHealthCheck({ path: '/health' });
}
}
119 changes: 119 additions & 0 deletions cloud/lib/auth-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
Mfa,
OAuthScope,
ProviderAttribute,
UserPool,
UserPoolClient,
UserPoolClientIdentityProvider,
UserPoolDomain,
UserPoolIdentityProviderSaml,
UserPoolIdentityProviderSamlMetadata,
} from 'aws-cdk-lib/aws-cognito';
import { CfnOutput, Duration, Stack, StackProps, Tags } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';

import { resourceName } from './resourceNamingUtils';

export class AuthStack extends Stack {
userPool: UserPool;
userPoolClient: UserPoolClient;
userPoolDomain: UserPoolDomain;

constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);

const azureTenantId = process.env.AZURE_TENANT_ID;
const azureApplicationId = process.env.AZURE_APPLICATION_ID;
if (!azureTenantId || !azureApplicationId) {
throw new Error(
'Need AZURE_TENANT_ID and AZURE_APPLICATION_ID environment vars!'
);
}

const generateResourceName = resourceName(scope);

// Cognito UserPool
const userPoolName = generateResourceName('userpool');
this.userPool = new UserPool(this, userPoolName, {
userPoolName,
enableSmsRole: false,
mfa: Mfa.OFF,
//email // not configured, we're not going to send email from here
signInCaseSensitive: false,
autoVerify: { email: false }, // will be sending email invite anyway
selfSignUpEnabled: false, // only users we explicity allow
standardAttributes: {
givenName: { required: true },
familyName: { required: true },
email: { required: true },
},
signInAliases: { email: true },
deletionProtection: false,
});
// Tags not correctly assigned from parent stack: https://github.com/aws/aws-cdk/issues/14127
Object.entries(props.tags ?? {}).forEach(([key, value]) => {
Tags.of(this.userPool).add(key, value);
});

new CfnOutput(this, 'UserPool.Identifier', {
value: `urn:amazon:cognito:sp:${this.userPool.userPoolId}`,
});
new CfnOutput(this, 'UserPool.ReplyUrl', {
value: `https://${userPoolName}.auth.${this.region}.amazoncognito.com/saml2/idpresponse`,
});

const idpName = generateResourceName('userpool-idp');
const identityProvider = new UserPoolIdentityProviderSaml(this, idpName, {
name: idpName,
idpSignout: true,
metadata: UserPoolIdentityProviderSamlMetadata.url(
`https://login.microsoftonline.com/${azureTenantId}/federationmetadata/2007-06/federationmetadata.xml?appid=${azureApplicationId}`
),
userPool: this.userPool,
attributeMapping: {
email: ProviderAttribute.other(
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
),
familyName: ProviderAttribute.other(
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'
),
givenName: ProviderAttribute.other(
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'
),
},
});

const userPoolClientName = generateResourceName('userpool-client');
this.userPoolClient = this.userPool.addClient(userPoolClientName, {
userPoolClientName,
authFlows: {
userSrp: true,
},
supportedIdentityProviders: [
UserPoolClientIdentityProvider.custom(identityProvider.providerName),
],
generateSecret: true,
oAuth: {
flows: {
authorizationCodeGrant: true,
},
scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
//callbackUrls,
//logoutUrls: callbackUrls,
},
accessTokenValidity: Duration.minutes(60),
idTokenValidity: Duration.minutes(60),
refreshTokenValidity: Duration.days(30),
authSessionValidity: Duration.minutes(3),
enableTokenRevocation: true,
preventUserExistenceErrors: true,
});

const userPoolDomainName = generateResourceName('userpool-domain');
this.userPoolDomain = this.userPool.addDomain(userPoolDomainName, {
cognitoDomain: {
domainPrefix: userPoolName,
},
});
}
}
1 change: 1 addition & 0 deletions cloud/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './resourceNamingUtils';
export { ApiStack } from './api-stack';
export { AuthStack } from './auth-stack';
12 changes: 12 additions & 0 deletions cloud/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"aws-cdk-lib": "^2.104.0",
"constructs": "^10.3.0",
"dotenv": "^16.3.1",
"source-map-support": "^0.5.21"
}
}

0 comments on commit c245172

Please sign in to comment.