Skip to content
Open
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 .changeset/stupid-dodos-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/auth-construct': patch
---

feat(auth): support custom domain for external identity provider
92 changes: 92 additions & 0 deletions packages/auth-construct/src/construct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,98 @@ void describe('Auth construct', () => {
],
});
});

describe('creates custom domain when custom domain options are present', () => {
const testHostedZoneName = 'test-zone-name';
const testHostedZoneId = 'test-zone-id';
const testCustomDomainName = 'test-custom-domain-name';

let template: Template;
let userPoolCustomDomainRefValue = '';
let userPoolCloudFrontDomainNameRefValue = '';

beforeEach(() => {
new AmplifyAuth(stack, 'test', {
name: 'test_name',
loginWith: {
email: true,
externalProviders: {
google: {
clientId: googleClientId,
clientSecret: SecretValue.unsafePlainText(googleClientSecret),
},
scopes: ['EMAIL', 'PROFILE'],
callbackUrls: ['http://callback.com'],
logoutUrls: ['http://logout.com'],
customDomainOptions: {
hostedZone: {
zoneName: testHostedZoneName,
hostedZoneId: testHostedZoneId,
},
domainName: testCustomDomainName,
},
domainPrefix: 'test-domain-prefix',
},
},
});
template = Template.fromStack(stack);
});

void it('creates certificate for the custom domain', () => {
template.hasResourceProperties('AWS::CertificateManager::Certificate', {
DomainName: testCustomDomainName,
DomainValidationOptions: [
{
DomainName: testCustomDomainName,
HostedZoneId: testHostedZoneId,
},
],
});
});

void it('creates custom domain for the user pool', () => {
const userPoolCustomDomains = Object.entries(
template.findResources('AWS::Cognito::UserPoolDomain', {
Properties: {
Domain: testCustomDomainName,
},
}),
);
assert.equal(userPoolCustomDomains.length, 1);
userPoolCustomDomainRefValue = userPoolCustomDomains[0][0];
});

void it('creates cloud front domain name for the user pool', () => {
const cloudFrontDomainNames = Object.entries(
template.findResources('Custom::UserPoolCloudFrontDomainName'),
);
assert.equal(cloudFrontDomainNames.length, 1);
userPoolCloudFrontDomainNameRefValue = cloudFrontDomainNames[0][0];
});

void it('creates dns record for the custom domain', () => {
template.hasResourceProperties('AWS::Route53::RecordSet', {
AliasTarget: {
DNSName: {
'Fn::GetAtt': [
userPoolCloudFrontDomainNameRefValue,
'DomainDescription.CloudFrontDistribution',
],
},
},
HostedZoneId: testHostedZoneId,
Type: 'A',
Name: `${testCustomDomainName}.${testHostedZoneName}.`,
});
});

void it('uses custom domain name for outputs', () => {
const outputs = template.findOutputs('*');
assert.deepEqual(outputs['oauthCognitoDomain']['Value'], {
Ref: userPoolCustomDomainRefValue,
});
});
});
});

void describe('storeOutput strategy', () => {
Expand Down
90 changes: 79 additions & 11 deletions packages/auth-construct/src/construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
AttributeMapping,
AuthProps,
CustomAttribute,
CustomDomainOptions,
CustomSmsSender,
EmailLoginSettings,
ExternalProviderOptions,
Expand All @@ -57,6 +58,12 @@ import {
import * as path from 'path';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
import { CDKContextKey } from '@aws-amplify/platform-core';
import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
import {
Certificate,
CertificateValidation,
} from 'aws-cdk-lib/aws-certificatemanager';
import { UserPoolDomainTarget } from 'aws-cdk-lib/aws-route53-targets';

type DefaultRoles = { auth: Role; unAuth: Role };
type IdentityProviderSetupResult = {
Expand Down Expand Up @@ -873,6 +880,38 @@ export class AmplifyAuth
return undefined;
};

private setupCustomDomain = (
userPool: UserPool,
customDomainOptions: CustomDomainOptions,
) => {
const hostedZone = HostedZone.fromHostedZoneAttributes(
this,
`${this.name}HostedZone`,
customDomainOptions.hostedZone,
);

const certificate = new Certificate(this, `${this.name}Certificate`, {
domainName: customDomainOptions.domainName,
validation: CertificateValidation.fromDns(hostedZone),
});

const customDomain = userPool.addDomain(
`${this.name}UserPoolCustomDomain`,
{
customDomain: {
domainName: customDomainOptions.domainName,
certificate,
},
},
);

new ARecord(this, `${this.name}ARecord`, {
zone: hostedZone,
recordName: customDomainOptions.domainName,
target: RecordTarget.fromAlias(new UserPoolDomainTarget(customDomain)),
});
};

/**
* Setup External Providers (OAuth/OIDC/SAML) and related settings
* such as OAuth settings and User Pool Domains
Expand Down Expand Up @@ -1074,6 +1113,11 @@ export class AmplifyAuth
);
}

// Generate a custom domain if custom domain options are specified
if (external.customDomainOptions) {
this.setupCustomDomain(this.userPool, external.customDomainOptions);
}

// oauth settings for the UserPool client
result.oAuthSettings = {
callbackUrls: external.callbackUrls,
Expand Down Expand Up @@ -1142,6 +1186,35 @@ export class AmplifyAuth
return result;
};

private getDomainName = (): string => {
const userPoolDomain = this.resources.userPool.node.tryFindChild(
`${this.name}UserPoolDomain`,
);
if (!userPoolDomain) {
return '';
}
if (!(userPoolDomain instanceof UserPoolDomain)) {
throw Error('Could not find UserPoolDomain resource in the stack.');
}
return `${userPoolDomain.domainName}.auth.${
Stack.of(this).region
}.amazoncognito.com`;
};

private getCustomDomainName = (): string => {
const userPoolCustomDomain = this.resources.userPool.node.tryFindChild(
`${this.name}UserPoolCustomDomain`,
);

if (!userPoolCustomDomain) {
return '';
}
if (!(userPoolCustomDomain instanceof UserPoolDomain)) {
throw Error('Could not find UserPoolCustomDomain resource in the stack.');
}
return userPoolCustomDomain.domainName;
};

/**
* Get sign-in policy configuration for passwordless authentication.
*/
Expand Down Expand Up @@ -1413,18 +1486,13 @@ export class AmplifyAuth

output.oauthCognitoDomain = Lazy.string({
produce: () => {
const userPoolDomain = this.resources.userPool.node.tryFindChild(
`${this.name}UserPoolDomain`,
);
if (!userPoolDomain) {
return '';
}
if (!(userPoolDomain instanceof UserPoolDomain)) {
throw Error('Could not find UserPoolDomain resource in the stack.');
const customDomainName = this.getCustomDomainName();

if (customDomainName === '') {
return this.getDomainName();
}
return `${userPoolDomain.domainName}.auth.${
Stack.of(this).region
}.amazoncognito.com`;

return customDomainName;
},
});

Expand Down
8 changes: 8 additions & 0 deletions packages/auth-construct/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UserPoolSESOptions,
} from 'aws-cdk-lib/aws-cognito';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { HostedZoneAttributes } from 'aws-cdk-lib/aws-route53';
export type VerificationEmailWithLink = {
/**
* The type of verification. Must be one of "CODE" or "LINK".
Expand Down Expand Up @@ -309,6 +310,11 @@ export type SamlProviderProps = Omit<
};
} & IdentityProviderProps;

export type CustomDomainOptions = {
hostedZone: HostedZoneAttributes;
domainName: string;
};

/**
* External provider options.
*/
Expand Down Expand Up @@ -374,6 +380,8 @@ export type ExternalProviderOptions = {
* List of allowed logout URLs for the identity providers.
*/
logoutUrls: string[];

customDomainOptions?: CustomDomainOptions;
};

/**
Expand Down