Skip to content

Commit 85d63aa

Browse files
committed
feat(auth): support custom domain for user pool
1 parent b402da9 commit 85d63aa

File tree

3 files changed

+179
-11
lines changed

3 files changed

+179
-11
lines changed

packages/auth-construct/src/construct.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,98 @@ void describe('Auth construct', () => {
13891389
],
13901390
});
13911391
});
1392+
1393+
describe('creates custom domain when custom domain options are present', () => {
1394+
const testHostedZoneName = 'test-zone-name';
1395+
const testHostedZoneId = 'test-zone-id';
1396+
const testCustomDomainName = 'test-custom-domain-name';
1397+
1398+
let template: Template;
1399+
let userPoolCustomDomainRefValue = '';
1400+
let userPoolCloudFrontDomainNameRefValue = '';
1401+
1402+
beforeEach(() => {
1403+
new AmplifyAuth(stack, 'test', {
1404+
name: 'test_name',
1405+
loginWith: {
1406+
email: true,
1407+
externalProviders: {
1408+
google: {
1409+
clientId: googleClientId,
1410+
clientSecret: SecretValue.unsafePlainText(googleClientSecret),
1411+
},
1412+
scopes: ['EMAIL', 'PROFILE'],
1413+
callbackUrls: ['http://callback.com'],
1414+
logoutUrls: ['http://logout.com'],
1415+
customDomainOptions: {
1416+
hostedZone: {
1417+
zoneName: testHostedZoneName,
1418+
hostedZoneId: testHostedZoneId,
1419+
},
1420+
domainName: testCustomDomainName,
1421+
},
1422+
domainPrefix: 'test-domain-prefix',
1423+
},
1424+
},
1425+
});
1426+
template = Template.fromStack(stack);
1427+
});
1428+
1429+
void it('creates certificate for the custom domain', () => {
1430+
template.hasResourceProperties('AWS::CertificateManager::Certificate', {
1431+
DomainName: testCustomDomainName,
1432+
DomainValidationOptions: [
1433+
{
1434+
DomainName: testCustomDomainName,
1435+
HostedZoneId: testHostedZoneId,
1436+
},
1437+
],
1438+
});
1439+
});
1440+
1441+
void it('creates custom domain for the user pool', () => {
1442+
const userPoolCustomDomains = Object.entries(
1443+
template.findResources('AWS::Cognito::UserPoolDomain', {
1444+
Properties: {
1445+
Domain: testCustomDomainName,
1446+
},
1447+
}),
1448+
);
1449+
assert.equal(userPoolCustomDomains.length, 1);
1450+
userPoolCustomDomainRefValue = userPoolCustomDomains[0][0];
1451+
});
1452+
1453+
void it('creates cloud front domain name for the user pool', () => {
1454+
const cloudFrontDomainNames = Object.entries(
1455+
template.findResources('Custom::UserPoolCloudFrontDomainName'),
1456+
);
1457+
assert.equal(cloudFrontDomainNames.length, 1);
1458+
userPoolCloudFrontDomainNameRefValue = cloudFrontDomainNames[0][0];
1459+
});
1460+
1461+
void it('creates dns record for the custom domain', () => {
1462+
template.hasResourceProperties('AWS::Route53::RecordSet', {
1463+
AliasTarget: {
1464+
DNSName: {
1465+
'Fn::GetAtt': [
1466+
userPoolCloudFrontDomainNameRefValue,
1467+
'DomainDescription.CloudFrontDistribution',
1468+
],
1469+
},
1470+
},
1471+
HostedZoneId: testHostedZoneId,
1472+
Type: 'A',
1473+
Name: `${testCustomDomainName}.${testHostedZoneName}.`,
1474+
});
1475+
});
1476+
1477+
void it('uses custom domain name for outputs', () => {
1478+
const outputs = template.findOutputs('*');
1479+
assert.deepEqual(outputs['oauthCognitoDomain']['Value'], {
1480+
Ref: userPoolCustomDomainRefValue,
1481+
});
1482+
});
1483+
});
13921484
});
13931485

13941486
void describe('storeOutput strategy', () => {

packages/auth-construct/src/construct.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
AttributeMapping,
4444
AuthProps,
4545
CustomAttribute,
46+
CustomDomainOptions,
4647
CustomSmsSender,
4748
EmailLoginSettings,
4849
ExternalProviderOptions,
@@ -55,6 +56,12 @@ import {
5556
} from '@aws-amplify/backend-output-storage';
5657
import * as path from 'path';
5758
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
59+
import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
60+
import {
61+
Certificate,
62+
CertificateValidation,
63+
} from 'aws-cdk-lib/aws-certificatemanager';
64+
import { UserPoolDomainTarget } from 'aws-cdk-lib/aws-route53-targets';
5865

5966
type DefaultRoles = { auth: Role; unAuth: Role };
6067
type IdentityProviderSetupResult = {
@@ -860,6 +867,38 @@ export class AmplifyAuth
860867
return undefined;
861868
};
862869

870+
private setupCustomDomain = (
871+
userPool: UserPool,
872+
customDomainOptions: CustomDomainOptions,
873+
) => {
874+
const hostedZone = HostedZone.fromHostedZoneAttributes(
875+
this,
876+
`${this.name}HostedZone`,
877+
customDomainOptions.hostedZone,
878+
);
879+
880+
const certificate = new Certificate(this, `${this.name}Certificate`, {
881+
domainName: customDomainOptions.domainName,
882+
validation: CertificateValidation.fromDns(hostedZone),
883+
});
884+
885+
const customDomain = userPool.addDomain(
886+
`${this.name}UserPoolCustomDomain`,
887+
{
888+
customDomain: {
889+
domainName: customDomainOptions.domainName,
890+
certificate,
891+
},
892+
},
893+
);
894+
895+
new ARecord(this, `${this.name}ARecord`, {
896+
zone: hostedZone,
897+
recordName: customDomainOptions.domainName,
898+
target: RecordTarget.fromAlias(new UserPoolDomainTarget(customDomain)),
899+
});
900+
};
901+
863902
/**
864903
* Setup External Providers (OAuth/OIDC/SAML) and related settings
865904
* such as OAuth settings and User Pool Domains
@@ -1061,6 +1100,11 @@ export class AmplifyAuth
10611100
);
10621101
}
10631102

1103+
// Generate a custom domain if custom domain options are specified
1104+
if (external.customDomainOptions) {
1105+
this.setupCustomDomain(this.userPool, external.customDomainOptions);
1106+
}
1107+
10641108
// oauth settings for the UserPool client
10651109
result.oAuthSettings = {
10661110
callbackUrls: external.callbackUrls,
@@ -1129,6 +1173,35 @@ export class AmplifyAuth
11291173
return result;
11301174
};
11311175

1176+
private getDomainName = (): string => {
1177+
const userPoolDomain = this.resources.userPool.node.tryFindChild(
1178+
`${this.name}UserPoolDomain`,
1179+
);
1180+
if (!userPoolDomain) {
1181+
return '';
1182+
}
1183+
if (!(userPoolDomain instanceof UserPoolDomain)) {
1184+
throw Error('Could not find UserPoolDomain resource in the stack.');
1185+
}
1186+
return `${userPoolDomain.domainName}.auth.${
1187+
Stack.of(this).region
1188+
}.amazoncognito.com`;
1189+
};
1190+
1191+
private getCustomDomainName = (): string => {
1192+
const userPoolCustomDomain = this.resources.userPool.node.tryFindChild(
1193+
`${this.name}UserPoolCustomDomain`,
1194+
);
1195+
1196+
if (!userPoolCustomDomain) {
1197+
return '';
1198+
}
1199+
if (!(userPoolCustomDomain instanceof UserPoolDomain)) {
1200+
throw Error('Could not find UserPoolCustomDomain resource in the stack.');
1201+
}
1202+
return userPoolCustomDomain.domainName;
1203+
};
1204+
11321205
/**
11331206
* Stores auth output using the provided strategy
11341207
*/
@@ -1285,18 +1358,13 @@ export class AmplifyAuth
12851358

12861359
output.oauthCognitoDomain = Lazy.string({
12871360
produce: () => {
1288-
const userPoolDomain = this.resources.userPool.node.tryFindChild(
1289-
`${this.name}UserPoolDomain`,
1290-
);
1291-
if (!userPoolDomain) {
1292-
return '';
1293-
}
1294-
if (!(userPoolDomain instanceof UserPoolDomain)) {
1295-
throw Error('Could not find UserPoolDomain resource in the stack.');
1361+
const customDomainName = this.getCustomDomainName();
1362+
1363+
if (customDomainName === '') {
1364+
return this.getDomainName();
12961365
}
1297-
return `${userPoolDomain.domainName}.auth.${
1298-
Stack.of(this).region
1299-
}.amazoncognito.com`;
1366+
1367+
return customDomainName;
13001368
},
13011369
});
13021370

packages/auth-construct/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
UserPoolSESOptions,
1111
} from 'aws-cdk-lib/aws-cognito';
1212
import { IFunction } from 'aws-cdk-lib/aws-lambda';
13+
import { HostedZoneAttributes } from 'aws-cdk-lib/aws-route53';
1314
export type VerificationEmailWithLink = {
1415
/**
1516
* The type of verification. Must be one of "CODE" or "LINK".
@@ -269,6 +270,11 @@ export type SamlProviderProps = Omit<
269270
};
270271
} & IdentityProviderProps;
271272

273+
export type CustomDomainOptions = {
274+
hostedZone: HostedZoneAttributes;
275+
domainName: string;
276+
};
277+
272278
/**
273279
* External provider options.
274280
*/
@@ -334,6 +340,8 @@ export type ExternalProviderOptions = {
334340
* List of allowed logout URLs for the identity providers.
335341
*/
336342
logoutUrls: string[];
343+
344+
customDomainOptions?: CustomDomainOptions;
337345
};
338346

339347
/**

0 commit comments

Comments
 (0)