Skip to content

Commit 33acc7c

Browse files
authored
feat(cognito): OpenID Connect identity provider (#20241)
Add the `UserPoolIdentityProviderOidc` class to create an OpenID Connect identity provider for user pools. ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent cedfde8 commit 33acc7c

File tree

11 files changed

+811
-2
lines changed

11 files changed

+811
-2
lines changed

packages/@aws-cdk/aws-cognito/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ The following third-party identity providers are currently supported in the CDK
503503
- [Facebook Login](https://developers.facebook.com/docs/facebook-login/)
504504
- [Google Login](https://developers.google.com/identity/sign-in/web/sign-in)
505505
- [Sign In With Apple](https://developer.apple.com/sign-in-with-apple/get-started/)
506+
- [OpenID Connect](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-oidc-idp.html)
506507

507508
The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity
508509
provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the

packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './base';
22
export * from './apple';
33
export * from './amazon';
44
export * from './facebook';
5-
export * from './google';
5+
export * from './google';
6+
export * from './oidc';
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { Names, Token } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
3+
import { CfnUserPoolIdentityProvider } from '../cognito.generated';
4+
import { UserPoolIdentityProviderProps } from './base';
5+
import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base';
6+
7+
/**
8+
* Properties to initialize UserPoolIdentityProviderOidc
9+
*/
10+
export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProviderProps {
11+
/**
12+
* The client id
13+
*/
14+
readonly clientId: string;
15+
16+
/**
17+
* The client secret
18+
*/
19+
readonly clientSecret: string;
20+
21+
/**
22+
* Issuer URL
23+
*/
24+
readonly issuerUrl: string;
25+
26+
/**
27+
* The name of the provider
28+
*
29+
* @default - the unique ID of the construct
30+
*/
31+
readonly name?: string;
32+
33+
/**
34+
* The OAuth 2.0 scopes that you will request from OpenID Connect. Scopes are
35+
* groups of OpenID Connect user attributes to exchange with your app.
36+
*
37+
* @default ['openid']
38+
*/
39+
readonly scopes?: string[];
40+
41+
/**
42+
* Identifiers
43+
*
44+
* Identifiers can be used to redirect users to the correct IdP in multitenant apps.
45+
*
46+
* @default - no identifiers used
47+
*/
48+
readonly identifiers?: string[]
49+
50+
/**
51+
* The method to use to request attributes
52+
*
53+
* @default OidcAttributeRequestMethod.GET
54+
*/
55+
readonly attributeRequestMethod?: OidcAttributeRequestMethod
56+
57+
/**
58+
* OpenID connect endpoints
59+
*
60+
* @default - auto discovered with issuer URL
61+
*/
62+
readonly endpoints?: OidcEndpoints;
63+
}
64+
65+
/**
66+
* OpenID Connect endpoints
67+
*/
68+
export interface OidcEndpoints {
69+
/**
70+
* Authorization endpoint
71+
*/
72+
readonly authorization: string;
73+
74+
/**
75+
* Token endpoint
76+
*/
77+
readonly token: string;
78+
79+
/**
80+
* UserInfo endpoint
81+
*/
82+
readonly userInfo: string;
83+
84+
/**
85+
* Jwks_uri endpoint
86+
*/
87+
readonly jwksUri: string;
88+
}
89+
90+
/**
91+
* The method to use to request attributes
92+
*/
93+
export enum OidcAttributeRequestMethod {
94+
/** GET */
95+
GET = 'GET',
96+
/** POST */
97+
POST = 'POST'
98+
}
99+
100+
/**
101+
* Represents a identity provider that integrates with OpenID Connect
102+
* @resource AWS::Cognito::UserPoolIdentityProvider
103+
*/
104+
export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase {
105+
public readonly providerName: string;
106+
107+
constructor(scope: Construct, id: string, props: UserPoolIdentityProviderOidcProps) {
108+
super(scope, id, props);
109+
110+
if (props.name && !Token.isUnresolved(props.name) && (props.name.length < 3 || props.name.length > 32)) {
111+
throw new Error(`Expected provider name to be between 3 and 32 characters, received ${props.name} (${props.name.length} characters)`);
112+
}
113+
114+
const scopes = props.scopes ?? ['openid'];
115+
116+
const resource = new CfnUserPoolIdentityProvider(this, 'Resource', {
117+
userPoolId: props.userPool.userPoolId,
118+
providerName: this.getProviderName(props.name),
119+
providerType: 'OIDC',
120+
providerDetails: {
121+
client_id: props.clientId,
122+
client_secret: props.clientSecret,
123+
authorize_scopes: scopes.join(' '),
124+
attributes_request_method: props.attributeRequestMethod ?? OidcAttributeRequestMethod.GET,
125+
oidc_issuer: props.issuerUrl,
126+
authorize_url: props.endpoints?.authorization,
127+
token_url: props.endpoints?.token,
128+
attributes_url: props.endpoints?.userInfo,
129+
jwks_uri: props.endpoints?.jwksUri,
130+
},
131+
idpIdentifiers: props.identifiers,
132+
attributeMapping: super.configureAttributeMapping(),
133+
});
134+
135+
this.providerName = super.getResourceNameAttribute(resource.ref);
136+
}
137+
138+
private getProviderName(name?: string): string {
139+
if (name) {
140+
if (!Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) {
141+
throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`);
142+
}
143+
return name;
144+
}
145+
146+
const uniqueId = Names.uniqueId(this);
147+
148+
if (uniqueId.length < 3) {
149+
return `${uniqueId}oidc`;
150+
}
151+
152+
if (uniqueId.length > 32) {
153+
return uniqueId.substring(0, 16) + uniqueId.substring(uniqueId.length - 16);
154+
}
155+
return uniqueId;
156+
}
157+
}

packages/@aws-cdk/aws-cognito/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@
122122
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps",
123123
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps",
124124
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps",
125-
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps"
125+
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps",
126+
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderOidcProps"
126127
]
127128
},
128129
"stability": "stable",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core';
2+
import { ProviderAttribute, UserPool, UserPoolIdentityProviderOidc } from '../lib';
3+
4+
/*
5+
* Stack verification steps
6+
* * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'cdk' sign in link shows up.
7+
*/
8+
const app = new App();
9+
const stack = new Stack(app, 'integ-user-pool-idp-google');
10+
11+
const userpool = new UserPool(stack, 'pool', {
12+
removalPolicy: RemovalPolicy.DESTROY,
13+
});
14+
15+
new UserPoolIdentityProviderOidc(stack, 'cdk', {
16+
userPool: userpool,
17+
name: 'cdk',
18+
clientId: 'client-id',
19+
clientSecret: 'client-secret',
20+
issuerUrl: 'https://www.issuer-url.com',
21+
endpoints: {
22+
authorization: 'https://www.issuer-url.com/authorize',
23+
token: 'https://www.issuer-url.com/token',
24+
userInfo: 'https://www.issuer-url.com/userinfo',
25+
jwksUri: 'https://www.issuer-url.com/jwks',
26+
},
27+
scopes: ['openid', 'phone'],
28+
attributeMapping: {
29+
phoneNumber: ProviderAttribute.other('phone_number'),
30+
},
31+
});
32+
33+
const client = userpool.addClient('client');
34+
35+
const domain = userpool.addDomain('domain', {
36+
cognitoDomain: {
37+
domainPrefix: 'cdk-test-pool',
38+
},
39+
});
40+
41+
new CfnOutput(stack, 'SignInLink', {
42+
value: domain.signInUrl(client, {
43+
redirectUri: 'https://example.com',
44+
}),
45+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"18.0.0"}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"Resources": {
3+
"pool056F3F7E": {
4+
"Type": "AWS::Cognito::UserPool",
5+
"Properties": {
6+
"AccountRecoverySetting": {
7+
"RecoveryMechanisms": [
8+
{
9+
"Name": "verified_phone_number",
10+
"Priority": 1
11+
},
12+
{
13+
"Name": "verified_email",
14+
"Priority": 2
15+
}
16+
]
17+
},
18+
"AdminCreateUserConfig": {
19+
"AllowAdminCreateUserOnly": true
20+
},
21+
"EmailVerificationMessage": "The verification code to your new account is {####}",
22+
"EmailVerificationSubject": "Verify your new account",
23+
"SmsVerificationMessage": "The verification code to your new account is {####}",
24+
"VerificationMessageTemplate": {
25+
"DefaultEmailOption": "CONFIRM_WITH_CODE",
26+
"EmailMessage": "The verification code to your new account is {####}",
27+
"EmailSubject": "Verify your new account",
28+
"SmsMessage": "The verification code to your new account is {####}"
29+
}
30+
},
31+
"UpdateReplacePolicy": "Delete",
32+
"DeletionPolicy": "Delete"
33+
},
34+
"poolclient2623294C": {
35+
"Type": "AWS::Cognito::UserPoolClient",
36+
"Properties": {
37+
"UserPoolId": {
38+
"Ref": "pool056F3F7E"
39+
},
40+
"AllowedOAuthFlows": [
41+
"implicit",
42+
"code"
43+
],
44+
"AllowedOAuthFlowsUserPoolClient": true,
45+
"AllowedOAuthScopes": [
46+
"profile",
47+
"phone",
48+
"email",
49+
"openid",
50+
"aws.cognito.signin.user.admin"
51+
],
52+
"CallbackURLs": [
53+
"https://example.com"
54+
],
55+
"SupportedIdentityProviders": [
56+
{
57+
"Ref": "cdk52888317"
58+
},
59+
"COGNITO"
60+
]
61+
}
62+
},
63+
"pooldomain430FA744": {
64+
"Type": "AWS::Cognito::UserPoolDomain",
65+
"Properties": {
66+
"Domain": "cdk-test-pool",
67+
"UserPoolId": {
68+
"Ref": "pool056F3F7E"
69+
}
70+
}
71+
},
72+
"cdk52888317": {
73+
"Type": "AWS::Cognito::UserPoolIdentityProvider",
74+
"Properties": {
75+
"ProviderName": "cdk",
76+
"ProviderType": "OIDC",
77+
"UserPoolId": {
78+
"Ref": "pool056F3F7E"
79+
},
80+
"AttributeMapping": {
81+
"phone_number": "phone_number"
82+
},
83+
"ProviderDetails": {
84+
"client_id": "client-id",
85+
"client_secret": "client-secret",
86+
"authorize_scopes": "openid phone",
87+
"attributes_request_method": "GET",
88+
"oidc_issuer": "https://www.issuer-url.com",
89+
"authorize_url": "https://www.issuer-url.com/authorize",
90+
"token_url": "https://www.issuer-url.com/token",
91+
"attributes_url": "https://www.issuer-url.com/userinfo",
92+
"jwks_uri": "https://www.issuer-url.com/jwks"
93+
}
94+
}
95+
}
96+
},
97+
"Outputs": {
98+
"SignInLink": {
99+
"Value": {
100+
"Fn::Join": [
101+
"",
102+
[
103+
"https://",
104+
{
105+
"Ref": "pooldomain430FA744"
106+
},
107+
".auth.",
108+
{
109+
"Ref": "AWS::Region"
110+
},
111+
".amazoncognito.com/login?client_id=",
112+
{
113+
"Ref": "poolclient2623294C"
114+
},
115+
"&response_type=code&redirect_uri=https://example.com"
116+
]
117+
]
118+
}
119+
}
120+
}
121+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": "18.0.0",
3+
"testCases": {
4+
"integ.user-pool-idp.oidc": {
5+
"stacks": [
6+
"integ-user-pool-idp-google"
7+
],
8+
"diffAssets": false,
9+
"stackUpdateWorkflow": true
10+
}
11+
},
12+
"synthContext": {},
13+
"enableLookups": false
14+
}

0 commit comments

Comments
 (0)