Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

909: Enable SSO Sign-in in cloud templates and (opt-in) UI #910

Merged
merged 3 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cloud/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
DOMAIN_NAME=your.domain
HOSTED_ZONE_ID=YOUR_AWS_HOSTED_ZONE_ID

# Enable Azure IdP
#IDP_NAME=azure
#AZURE_APPLICATION_ID=[your-azure-application-id]
#AZURE_TENANT_ID=[your-azure-tenant-id]
2 changes: 0 additions & 2 deletions cloud/bin/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ const authStack = new AuthStack(app, generateStackName('auth'), {
description: generateDescription('Auth stack'),
env,
tags,
authDomainName: certificateStack.authDomainName,
certificate: certificateStack.cloudFrontCert,
hostedZone: hostedZoneStack.hostedZone,
});

Expand Down
120 changes: 62 additions & 58 deletions cloud/lib/auth-stack.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import {
AllowedMethods,
CachePolicy,
Distribution,
OriginRequestPolicy,
PriceClass,
ResponseHeadersPolicy,
ViewerProtocolPolicy,
} from 'aws-cdk-lib/aws-cloudfront';
import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
import { Mfa, OAuthScope, UserPool } from 'aws-cdk-lib/aws-cognito';
import { ARecord, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
Mfa,
OAuthScope,
ProviderAttribute,
UserPool,
UserPoolClientIdentityProvider,
UserPoolIdentityProviderSaml,
UserPoolIdentityProviderSamlMetadata,
} from 'aws-cdk-lib/aws-cognito';
import { IHostedZone } from 'aws-cdk-lib/aws-route53';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { CfnOutput, Duration, RemovalPolicy, Stack, StackProps, Tags } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';

import { resourceId, stageName } from './resourceNamingUtils';
import { appName, resourceId, stageName } from './resourceNamingUtils';

type AuthStackProps = StackProps & {
certificate: ICertificate;
hostedZone: IHostedZone;
authDomainName: string;
};

export class AuthStack extends Stack {
Expand All @@ -36,12 +30,14 @@ export class AuthStack extends Stack {
const stage = stageName(scope);
const generateResourceId = resourceId(scope);

const { certificate, env, hostedZone, authDomainName } = props;
const { env, hostedZone } = props;
if (!env?.region) {
throw new Error('Region not defined in stack env, cannot continue!');
}

// Cognito UserPool
/*
User Pool - including attribute claims and password policy
*/
const userPool = new UserPool(this, generateResourceId('userpool'), {
enableSmsRole: false,
mfa: Mfa.OFF,
Expand Down Expand Up @@ -69,12 +65,50 @@ export class AuthStack extends Stack {
Tags.of(userPool).add(key, value);
});

new CfnOutput(this, 'UserPool.Id', {
value: `urn:amazon:cognito:sp:${userPool.userPoolId}`,
/*
Identity Providers - currently only Cognito itself plus Azure
*/
const supportedIdentityProviders = [UserPoolClientIdentityProvider.COGNITO];

if (process.env.IDP_NAME?.toUpperCase() === 'AZURE') {
const { AZURE_APPLICATION_ID, AZURE_TENANT_ID } = process.env;
if (!AZURE_APPLICATION_ID) throw new Error('Missing env var AZURE_APPLICATION_SECRET');
if (!AZURE_TENANT_ID) throw new Error('Missing env var AZURE_TENANT_ID');
const idp = new UserPoolIdentityProviderSaml(this, generateResourceId('azure-idp'), {
name: 'Azure',
userPool,
metadata: UserPoolIdentityProviderSamlMetadata.url(
`https://login.microsoftonline.com/${AZURE_TENANT_ID}/federationmetadata/2007-06/federationmetadata.xml?appid=${AZURE_APPLICATION_ID}`
),
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'
),
},
});
supportedIdentityProviders.push(UserPoolClientIdentityProvider.custom(idp.providerName));
}

/*
User Pool Domain - where authentication service is reachable
*/
const userPoolDomain = userPool.addDomain(generateResourceId('userpool-domain'), {
cognitoDomain: {
domainPrefix: appName.toLowerCase(),
},
});

/*
User Pool Client - defines Auth flow and token validity
*/
const logoutUrls = [`https://${hostedZone.zoneName}`];
const callbackUrls = logoutUrls.concat(`https://api.${hostedZone.zoneName}/oauth2/idpresponse`);
const replyUrl = `${userPoolDomain.baseUrl()}/saml2/idpresponse`;
const userPoolClient = userPool.addClient(generateResourceId('userpool-client'), {
authFlows: {
userSrp: true,
Expand All @@ -84,57 +118,27 @@ export class AuthStack extends Stack {
authorizationCodeGrant: true,
},
scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
callbackUrls,
callbackUrls: logoutUrls.concat(replyUrl),
logoutUrls,
},
supportedIdentityProviders,
accessTokenValidity: Duration.minutes(60),
idTokenValidity: Duration.minutes(60),
refreshTokenValidity: Duration.days(14),
enableTokenRevocation: true,
preventUserExistenceErrors: true,
});

userPool.addDomain(generateResourceId('userpool-domain'), {
cognitoDomain: {
domainPrefix: generateResourceId('auth'),
},
new CfnOutput(this, 'UserPool.Id', {
value: `urn:amazon:cognito:sp:${userPool.userPoolId}`,
});
new CfnOutput(this, 'UserPool.ReplyURL', {
value: replyUrl,
});

new CfnOutput(this, 'UserPoolClient.Id', {
value: userPoolClient.userPoolClientId,
});

/*
Use a CloudFront distribution to proxy Cognito.

Why? Turns out a custom userpool domain won't work due to CORS failures :(
Cognito does that by creating a CloudFront distro under the hood, so we're
not adding any costs by configuring this manually.
*/
const cognitoProxyDistribution = new Distribution(this, generateResourceId('cognito-proxy'), {
certificate,
domainNames: [authDomainName],
enableLogging: true,
defaultBehavior: {
origin: new HttpOrigin(`cognito-idp.${env.region}.amazonaws.com`),
allowedMethods: AllowedMethods.ALLOW_ALL,
cachePolicy: CachePolicy.CACHING_DISABLED,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_AND_CLOUDFRONT_2022,
responseHeadersPolicy:
ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS,
},
priceClass: PriceClass.PRICE_CLASS_100,
});

new ARecord(this, generateResourceId('arecord-auth-proxy'), {
zone: hostedZone,
recordName: authDomainName,
target: RecordTarget.fromAlias(new CloudFrontTarget(cognitoProxyDistribution)),
deleteExisting: true,
comment: 'DNS A Record for Cognito auth proxy',
});

// SSM Parameters accessed in auth edge lambda, as cannot use ENV vars.
this.parameterNameUserPoolId = `/${stage}/userpool-id`;
new StringParameter(this, generateResourceId('parameter-userpool-id'), {
Expand Down
3 changes: 0 additions & 3 deletions cloud/lib/certificate-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type CertificateStackProps = StackProps & {

export class CertificateStack extends Stack {
public readonly apiDomainName: string;
public readonly authDomainName: string;
public readonly cloudFrontCert: ICertificate;
public readonly loadBalancerCert: ICertificate;

Expand All @@ -27,13 +26,11 @@ export class CertificateStack extends Stack {
const generateResourceId = resourceId(scope);
const validation = CertificateValidation.fromDns(hostedZone);
this.apiDomainName = `api.${hostedZone.zoneName}`;
this.authDomainName = `auth.${hostedZone.zoneName}`;

// Yes this is deprecated, but CDK currently gives us no way to use
// Permissions Boundaries with cross-region resources, so ...
this.cloudFrontCert = new DnsValidatedCertificate(this, generateResourceId('cert-ui'), {
domainName: hostedZone.zoneName,
subjectAlternativeNames: [this.authDomainName],
hostedZone,
validation,
region: 'us-east-1',
Expand Down
1 change: 0 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ VITE_BACKEND_URL=http://localhost:3000/api
#VITE_COGNITO_USERPOOL_ID=YOUR_USERPOOL_ID
#VITE_COGNITO_USERPOOL_CLIENT=YOUR_CLIENT_ID
#VITE_COGNITO_USERPOOL_DOMAIN=YOUR_AUTH_DOMAIN
#VITE_COGNITO_USERPOOL_ENDPOINT=https://${VITE_COGNITO_USERPOOL_DOMAIN}
23 changes: 23 additions & 0 deletions frontend/src/assets/icons/Azure.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading