-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontrol-plane-stack.ts
More file actions
85 lines (76 loc) · 3.74 KB
/
Copy pathcontrol-plane-stack.ts
File metadata and controls
85 lines (76 loc) · 3.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { CfnOutput, RemovalPolicy, Stack, StackProps, Tags } from 'aws-cdk-lib';
import { AccountRecovery, Mfa, OAuthScope, UserPool, UserPoolClient, UserPoolDomain, VerificationEmailStyle } from 'aws-cdk-lib/aws-cognito';
import { Construct } from 'constructs';
import { EnvName } from './env';
export interface ControlPlaneStackProps extends StackProps {
readonly envName: EnvName;
/** Hosted UI redirect targets for this environment's web app. Never defaulted outside
* `dev` — see `bin/app.ts`'s `resolveCallbackUrls`, which fails closed rather than let a
* stray placeholder URL reach a real environment's synthesized template. */
readonly callbackUrls: string[];
readonly logoutUrls: string[];
}
/**
* The control plane's identity boundary (roadmap architecture diagram; AD2/ADR-0003): a Cognito
* user pool whose `sub` claim becomes `openjobradar.tenancy.context.TenantContext.user_id`
* everywhere downstream. Authentication only — authorization scoping is what `TenantContext`
* and `TenantRepository` already do in Python (ADR-0016); this stack's whole job is producing a
* `sub` a browser can obtain, not deciding what that `sub` may touch.
*
* Security defaults follow the roadmap's M3 shape directly rather than CDK's library defaults:
* self-service sign-up with required email verification, a 12-character password floor,
* optional TOTP MFA (no SMS — the roadmap's email-first deliverability posture, AD7, extends to
* not taking on SMS carrier cost/reliability for auth either), and a public SPA app client
* (`generateSecret: false`) using the authorization-code-with-PKCE flow — no client secret ever
* ships in browser code. `preventUserExistenceErrors` is on so a failed sign-in can't be used to
* enumerate registered emails.
*/
export class ControlPlaneStack extends Stack {
public readonly userPool: UserPool;
public readonly userPoolDomain: UserPoolDomain;
public readonly userPoolClient: UserPoolClient;
constructor(scope: Construct, id: string, props: ControlPlaneStackProps) {
super(scope, id, props);
const retain = props.envName === 'prod';
this.userPool = new UserPool(this, 'UserPool', {
userPoolName: `openjobradar-${props.envName}`,
selfSignUpEnabled: true,
signInAliases: { email: true },
autoVerify: { email: true },
standardAttributes: {
email: { required: true, mutable: true },
},
passwordPolicy: {
minLength: 12,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
},
accountRecovery: AccountRecovery.EMAIL_ONLY,
mfa: Mfa.OPTIONAL,
mfaSecondFactor: { sms: false, otp: true },
userVerification: { emailStyle: VerificationEmailStyle.CODE },
removalPolicy: retain ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
});
this.userPoolDomain = this.userPool.addDomain('HostedUiDomain', {
cognitoDomain: { domainPrefix: `openjobradar-${props.envName}` },
});
this.userPoolClient = this.userPool.addClient('WebAppClient', {
generateSecret: false,
authFlows: { userSrp: true },
oAuth: {
flows: { authorizationCodeGrant: true },
scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
callbackUrls: props.callbackUrls,
logoutUrls: props.logoutUrls,
},
preventUserExistenceErrors: true,
});
Tags.of(this).add('openjobradar:env', props.envName);
Tags.of(this).add('openjobradar:stack', 'control-plane');
new CfnOutput(this, 'UserPoolId', { value: this.userPool.userPoolId });
new CfnOutput(this, 'UserPoolClientId', { value: this.userPoolClient.userPoolClientId });
new CfnOutput(this, 'HostedUiDomain', { value: this.userPoolDomain.domainName });
}
}