Skip to content

Commit

Permalink
feat(cognito): OpenID Connect identity provider (#20241)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
jogold authored May 31, 2022
1 parent cedfde8 commit 33acc7c
Show file tree
Hide file tree
Showing 11 changed files with 811 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ The following third-party identity providers are currently supported in the CDK
- [Facebook Login](https://developers.facebook.com/docs/facebook-login/)
- [Google Login](https://developers.google.com/identity/sign-in/web/sign-in)
- [Sign In With Apple](https://developer.apple.com/sign-in-with-apple/get-started/)
- [OpenID Connect](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-oidc-idp.html)

The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity
provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './base';
export * from './apple';
export * from './amazon';
export * from './facebook';
export * from './google';
export * from './google';
export * from './oidc';
157 changes: 157 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idps/oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Names, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';
import { UserPoolIdentityProviderProps } from './base';
import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base';

/**
* Properties to initialize UserPoolIdentityProviderOidc
*/
export interface UserPoolIdentityProviderOidcProps extends UserPoolIdentityProviderProps {
/**
* The client id
*/
readonly clientId: string;

/**
* The client secret
*/
readonly clientSecret: string;

/**
* Issuer URL
*/
readonly issuerUrl: string;

/**
* The name of the provider
*
* @default - the unique ID of the construct
*/
readonly name?: string;

/**
* The OAuth 2.0 scopes that you will request from OpenID Connect. Scopes are
* groups of OpenID Connect user attributes to exchange with your app.
*
* @default ['openid']
*/
readonly scopes?: string[];

/**
* Identifiers
*
* Identifiers can be used to redirect users to the correct IdP in multitenant apps.
*
* @default - no identifiers used
*/
readonly identifiers?: string[]

/**
* The method to use to request attributes
*
* @default OidcAttributeRequestMethod.GET
*/
readonly attributeRequestMethod?: OidcAttributeRequestMethod

/**
* OpenID connect endpoints
*
* @default - auto discovered with issuer URL
*/
readonly endpoints?: OidcEndpoints;
}

/**
* OpenID Connect endpoints
*/
export interface OidcEndpoints {
/**
* Authorization endpoint
*/
readonly authorization: string;

/**
* Token endpoint
*/
readonly token: string;

/**
* UserInfo endpoint
*/
readonly userInfo: string;

/**
* Jwks_uri endpoint
*/
readonly jwksUri: string;
}

/**
* The method to use to request attributes
*/
export enum OidcAttributeRequestMethod {
/** GET */
GET = 'GET',
/** POST */
POST = 'POST'
}

/**
* Represents a identity provider that integrates with OpenID Connect
* @resource AWS::Cognito::UserPoolIdentityProvider
*/
export class UserPoolIdentityProviderOidc extends UserPoolIdentityProviderBase {
public readonly providerName: string;

constructor(scope: Construct, id: string, props: UserPoolIdentityProviderOidcProps) {
super(scope, id, props);

if (props.name && !Token.isUnresolved(props.name) && (props.name.length < 3 || props.name.length > 32)) {
throw new Error(`Expected provider name to be between 3 and 32 characters, received ${props.name} (${props.name.length} characters)`);
}

const scopes = props.scopes ?? ['openid'];

const resource = new CfnUserPoolIdentityProvider(this, 'Resource', {
userPoolId: props.userPool.userPoolId,
providerName: this.getProviderName(props.name),
providerType: 'OIDC',
providerDetails: {
client_id: props.clientId,
client_secret: props.clientSecret,
authorize_scopes: scopes.join(' '),
attributes_request_method: props.attributeRequestMethod ?? OidcAttributeRequestMethod.GET,
oidc_issuer: props.issuerUrl,
authorize_url: props.endpoints?.authorization,
token_url: props.endpoints?.token,
attributes_url: props.endpoints?.userInfo,
jwks_uri: props.endpoints?.jwksUri,
},
idpIdentifiers: props.identifiers,
attributeMapping: super.configureAttributeMapping(),
});

this.providerName = super.getResourceNameAttribute(resource.ref);
}

private getProviderName(name?: string): string {
if (name) {
if (!Token.isUnresolved(name) && (name.length < 3 || name.length > 32)) {
throw new Error(`Expected provider name to be between 3 and 32 characters, received ${name} (${name.length} characters)`);
}
return name;
}

const uniqueId = Names.uniqueId(this);

if (uniqueId.length < 3) {
return `${uniqueId}oidc`;
}

if (uniqueId.length > 32) {
return uniqueId.substring(0, 16) + uniqueId.substring(uniqueId.length - 16);
}
return uniqueId;
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-cognito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps",
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps",
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps",
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps"
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps",
"props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderOidcProps"
]
},
"stability": "stable",
Expand Down
45 changes: 45 additions & 0 deletions packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core';
import { ProviderAttribute, UserPool, UserPoolIdentityProviderOidc } from '../lib';

/*
* Stack verification steps
* * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'cdk' sign in link shows up.
*/
const app = new App();
const stack = new Stack(app, 'integ-user-pool-idp-google');

const userpool = new UserPool(stack, 'pool', {
removalPolicy: RemovalPolicy.DESTROY,
});

new UserPoolIdentityProviderOidc(stack, 'cdk', {
userPool: userpool,
name: 'cdk',
clientId: 'client-id',
clientSecret: 'client-secret',
issuerUrl: 'https://www.issuer-url.com',
endpoints: {
authorization: 'https://www.issuer-url.com/authorize',
token: 'https://www.issuer-url.com/token',
userInfo: 'https://www.issuer-url.com/userinfo',
jwksUri: 'https://www.issuer-url.com/jwks',
},
scopes: ['openid', 'phone'],
attributeMapping: {
phoneNumber: ProviderAttribute.other('phone_number'),
},
});

const client = userpool.addClient('client');

const domain = userpool.addDomain('domain', {
cognitoDomain: {
domainPrefix: 'cdk-test-pool',
},
});

new CfnOutput(stack, 'SignInLink', {
value: domain.signInUrl(client, {
redirectUri: 'https://example.com',
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"18.0.0"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
{
"Resources": {
"pool056F3F7E": {
"Type": "AWS::Cognito::UserPool",
"Properties": {
"AccountRecoverySetting": {
"RecoveryMechanisms": [
{
"Name": "verified_phone_number",
"Priority": 1
},
{
"Name": "verified_email",
"Priority": 2
}
]
},
"AdminCreateUserConfig": {
"AllowAdminCreateUserOnly": true
},
"EmailVerificationMessage": "The verification code to your new account is {####}",
"EmailVerificationSubject": "Verify your new account",
"SmsVerificationMessage": "The verification code to your new account is {####}",
"VerificationMessageTemplate": {
"DefaultEmailOption": "CONFIRM_WITH_CODE",
"EmailMessage": "The verification code to your new account is {####}",
"EmailSubject": "Verify your new account",
"SmsMessage": "The verification code to your new account is {####}"
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"poolclient2623294C": {
"Type": "AWS::Cognito::UserPoolClient",
"Properties": {
"UserPoolId": {
"Ref": "pool056F3F7E"
},
"AllowedOAuthFlows": [
"implicit",
"code"
],
"AllowedOAuthFlowsUserPoolClient": true,
"AllowedOAuthScopes": [
"profile",
"phone",
"email",
"openid",
"aws.cognito.signin.user.admin"
],
"CallbackURLs": [
"https://example.com"
],
"SupportedIdentityProviders": [
{
"Ref": "cdk52888317"
},
"COGNITO"
]
}
},
"pooldomain430FA744": {
"Type": "AWS::Cognito::UserPoolDomain",
"Properties": {
"Domain": "cdk-test-pool",
"UserPoolId": {
"Ref": "pool056F3F7E"
}
}
},
"cdk52888317": {
"Type": "AWS::Cognito::UserPoolIdentityProvider",
"Properties": {
"ProviderName": "cdk",
"ProviderType": "OIDC",
"UserPoolId": {
"Ref": "pool056F3F7E"
},
"AttributeMapping": {
"phone_number": "phone_number"
},
"ProviderDetails": {
"client_id": "client-id",
"client_secret": "client-secret",
"authorize_scopes": "openid phone",
"attributes_request_method": "GET",
"oidc_issuer": "https://www.issuer-url.com",
"authorize_url": "https://www.issuer-url.com/authorize",
"token_url": "https://www.issuer-url.com/token",
"attributes_url": "https://www.issuer-url.com/userinfo",
"jwks_uri": "https://www.issuer-url.com/jwks"
}
}
}
},
"Outputs": {
"SignInLink": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "pooldomain430FA744"
},
".auth.",
{
"Ref": "AWS::Region"
},
".amazoncognito.com/login?client_id=",
{
"Ref": "poolclient2623294C"
},
"&response_type=code&redirect_uri=https://example.com"
]
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "18.0.0",
"testCases": {
"integ.user-pool-idp.oidc": {
"stacks": [
"integ-user-pool-idp-google"
],
"diffAssets": false,
"stackUpdateWorkflow": true
}
},
"synthContext": {},
"enableLookups": false
}
Loading

0 comments on commit 33acc7c

Please sign in to comment.