Skip to content

Commit

Permalink
Merge pull request #213 from aws-samples/add/custom-oidc-provider
Browse files Browse the repository at this point in the history
Feature: custom OIDC provider
  • Loading branch information
wadabee authored Apr 5, 2024
2 parents f6ea681 + 98ba8d6 commit 5189895
Show file tree
Hide file tree
Showing 16 changed files with 442 additions and 133 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ const userPool = new UserPool(this, "UserPool", {

### External Identity Provider

This sample supports external identity provider. Currently we only support Google. To set up, See [SETUP_IDP.md](./docs/SETUP_IDP.md).
This sample supports external identity provider. Currently we support [Google](./docs/idp/SET_UP_GOOGLE.md) and [custom OIDC provider](./docs/idp/SET_UP_CUSTOM_OIDC.md).

### Encrypt Aurora Serverless storage

Expand Down
100 changes: 66 additions & 34 deletions cdk/lib/constructs/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {
UserPoolClient,
UserPoolIdentityProviderGoogle,
CfnUserPoolGroup,
UserPoolIdentityProviderOidc,
} from "aws-cdk-lib/aws-cognito";
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";

import { Construct } from "constructs";
import { Idp } from "../utils/identityProvider";
import { Idp, TIdentityProvider } from "../utils/identityProvider";

export interface AuthProps {
readonly origin: string;
Expand All @@ -29,7 +30,8 @@ export class Auth extends Construct {
requireDigits: true,
minLength: 8,
},
selfSignUpEnabled: true,
// Disable if identity providers are configured
selfSignUpEnabled: !props.idp.isExist(),
signInAliases: {
username: false,
email: true,
Expand Down Expand Up @@ -59,43 +61,73 @@ export class Auth extends Construct {

const client = userPool.addClient(`Client`, clientProps);

if (props.idp.isExist()) {
for (const provider of props.idp.getProviders()) {
switch (provider.service) {
case "google": {
const secret = secretsmanager.Secret.fromSecretNameV2(
this,
"Secret",
provider.secretName
);
const configureProvider = (
provider: TIdentityProvider,
userPool: UserPool,
client: UserPoolClient
) => {
const secret = secretsmanager.Secret.fromSecretNameV2(
this,
"Secret",
provider.secretName
);

const clientId = secret
.secretValueFromJson("clientId")
.unsafeUnwrap()
.toString();
const clientSecret = secret.secretValueFromJson("clientSecret");
const clientId = secret
.secretValueFromJson("clientId")
.unsafeUnwrap()
.toString();
const clientSecret = secret.secretValueFromJson("clientSecret");

const googleProvider = new UserPoolIdentityProviderGoogle(
this,
"GoogleProvider",
{
userPool,
clientId: clientId,
clientSecretValue: clientSecret,
scopes: ["openid", "email"],
attributeMapping: {
email: ProviderAttribute.GOOGLE_EMAIL,
},
}
);
switch (provider.service) {
// Currently only Google and custom OIDC are supported
case "google": {
const googleProvider = new UserPoolIdentityProviderGoogle(
this,
"GoogleProvider",
{
userPool,
clientId,
clientSecretValue: clientSecret,
scopes: ["openid", "email"],
attributeMapping: {
email: ProviderAttribute.GOOGLE_EMAIL,
},
}
);
client.node.addDependency(googleProvider);
}
case "oidc": {
const issuerUrl = secret
.secretValueFromJson("issuerUrl")
.unsafeUnwrap()
.toString();

client.node.addDependency(googleProvider);
}
// set other providers
default:
continue;
const oidcProvider = new UserPoolIdentityProviderOidc(
this,
"OidcProvider",
{
name: provider.serviceName,
userPool,
clientId,
clientSecret: clientSecret.unsafeUnwrap().toString(),
issuerUrl,
attributeMapping: {
// This is an example of mapping the email attribute.
// Replace this with the actual idp attribute key.
email: ProviderAttribute.other("EMAIL"),
},
scopes: ["openid", "email"],
}
);
client.node.addDependency(oidcProvider);
}
}
};

if (props.idp.isExist()) {
for (const provider of props.idp.getProviders()) {
configureProvider(provider, userPool, client);
}

userPool.addDomain("UserPool", {
cognitoDomain: {
Expand Down
2 changes: 2 additions & 0 deletions cdk/lib/constructs/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export class Frontend extends Construct {
VITE_APP_REDIRECT_SIGNOUT_URL: this.getOrigin(),
VITE_APP_COGNITO_DOMAIN: cognitoDomain,
VITE_APP_SOCIAL_PROVIDERS: idp.getSocialProviders(),
VITE_APP_CUSTOM_PROVIDER_ENABLED: idp.checkCustomProviderEnabled(),
VITE_APP_CUSTOM_PROVIDER_NAME: idp.getCustomProviderName(),
};
return { ...defaultProps, ...oAuthProps };
})();
Expand Down
55 changes: 46 additions & 9 deletions cdk/lib/utils/identityProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { aws_cognito } from "aws-cdk-lib";
export type Idp = ReturnType<typeof identityProvider>;

export type TIdentityProvider = {
/**
* Service name for social providers.
*/
service: string;
/**
* Service name for OIDC. Required when service is "oidc"
*/
serviceName?: string;
/**
* Secret name of the secret in Secrets Manager.
*/
secretName: string;
};

Expand Down Expand Up @@ -39,8 +49,11 @@ export const identityProvider = (identityProviders: TIdentityProvider[]) => {
};

const getSupportedIndetityProviders = () => {
return [...getProviders(), { service: "cognito" }].map(({ service }) => {
switch (service) {
return [
...getProviders(),
{ service: "cognito", secretName: "" } as TIdentityProvider,
].map((provider) => {
switch (provider.service) {
case "google":
return aws_cognito.UserPoolClientIdentityProvider.GOOGLE;
case "facebook":
Expand All @@ -51,22 +64,37 @@ export const identityProvider = (identityProviders: TIdentityProvider[]) => {
return aws_cognito.UserPoolClientIdentityProvider.APPLE;
case "cognito":
return aws_cognito.UserPoolClientIdentityProvider.COGNITO;
case "oidc":
return aws_cognito.UserPoolClientIdentityProvider.custom(
provider.serviceName! // already validated
);
default:
throw new Error(`Invalid identity provider: ${service}`);
throw new Error(`Invalid identity provider: ${provider.service}`);
}
});
};

const getSocialProviders = () =>
getProviders()
.filter(({ service }) => service !== "oidc")
.map(({ service }) => service)
.join(",");

const checkCustomProviderEnabled = () =>
// Currently only support OIDC provider (SAML not supported)
getProviders().some(({ service }) => service === "oidc");

const getCustomProviderName = () =>
// Currently only support OIDC provider (SAML not supported)
getProviders().find(({ service }) => service === "oidc")?.serviceName;

return {
isExist,
getProviders,
getSupportedIndetityProviders,
getSocialProviders,
checkCustomProviderEnabled,
getCustomProviderName,
};
};

Expand All @@ -77,12 +105,21 @@ const validateSocialProvider = (
provider: TIdentityProvider
):
| Effect.Effect<never, InvalidSocialProvider, never>
| Effect.Effect<TIdentityProvider, never, never> =>
!["google", "facebook", "amazon", "apple"].includes(provider.service)
? Effect.fail({
type: "InvalidSocialProvider",
})
: Effect.succeed(provider);
| Effect.Effect<TIdentityProvider, never, never> => {
if (
!["google", "facebook", "amazon", "apple", "oidc"].includes(
provider.service
)
) {
return Effect.fail({ type: "InvalidSocialProvider" });
}

if (provider.service === "oidc" && !provider.serviceName) {
return Effect.fail({ type: "InvalidSocialProvider" });
}

return Effect.succeed(provider);
};

const isIdpAsArray = (
identityProviders: TIdentityProvider[]
Expand Down
46 changes: 46 additions & 0 deletions cdk/test/cdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,52 @@ describe("Fine-grained Assertions Test", () => {
);
});

test("Custom OIDC Provider Generation", () => {
const app = new cdk.App();
const domainPrefix = "test-domain";
const hasOidcProviderStack = new BedrockChatStack(
app,
"OidcProviderGenerateStack",
{
bedrockRegion: "us-east-1",
crossRegionReferences: true,
webAclId: "",
enableUsageAnalysis: true,
identityProviders: [
{
secretName: "MyOidcTestSecret",
service: "oidc",
serviceName: "MyOidcProvider",
},
],
userPoolDomainPrefix: domainPrefix,
publishedApiAllowedIpV4AddressRanges: [""],
publishedApiAllowedIpV6AddressRanges: [""],
}
);
const hasOidcProviderTemplate = Template.fromStack(hasOidcProviderStack);

hasOidcProviderTemplate.hasResourceProperties(
"AWS::Cognito::UserPoolDomain",
{
Domain: domainPrefix,
}
);

hasOidcProviderTemplate.hasResourceProperties(
"AWS::Cognito::UserPoolClient",
{
SupportedIdentityProviders: ["MyOidcProvider", "COGNITO"],
}
);
hasOidcProviderTemplate.hasResourceProperties(
"AWS::Cognito::UserPoolIdentityProvider",
{
ProviderType: "OIDC",
}
);
});

test("default stack", () => {
const app = new cdk.App();

Expand Down
2 changes: 1 addition & 1 deletion docs/README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ const userPool = new UserPool(this, "UserPool", {

### 外部のアイデンティティプロバイダー

このサンプルは外部のアイデンティティプロバイダーをサポートしています。現在、Google のみをサポートしています。設定するには、[こちら](./SET_UP_IDP_ja.md)をご覧ください
このサンプルは外部のアイデンティティプロバイダーをサポートしています。現在、[Google](./idp/SET_UP_GOOGLE_ja.md)および[カスタム OIDC プロバイダー](./idp/SET_UP_CUSTOM_OIDC.md)をサポートしています

### ローカルでの開発について

Expand Down
63 changes: 63 additions & 0 deletions docs/idp/SET_UP_CUSTOM_OIDC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Set up external identity provider

## Step 1: Create an OIDC Client

Follow the procedures for the target OIDC provider, and note the values for the OIDC client ID and secret. Also issuer URL is required on the following steps. If redirect URI is needed for the setup process, enter dummy value, which will be replaced after deployment completed.

## Step 2: Store Credentials in AWS Secrets Manager

1. Go to the AWS Management Console.
2. Navigate to Secrets Manager and choose "Store a new secret".
3. Select "Other type of secrets".
4. Input the client ID and client secret as key-value pairs.

- Key: `clientId`, Value: <YOUR_GOOGLE_CLIENT_ID>
- Key: `clientSecret`, Value: <YOUR_GOOGLE_CLIENT_SECRET>
- Key: `issuerUrl`, Value: <ISSUER_URL_OF_THE_PROVIDER>

5. Follow the prompts to name and describe the secret. Note the secret name as you will need it in your CDK code (Used in Step 3 variable name <YOUR_SECRET_NAME>).
6. Review and store the secret.

### Attention

The key names must exactly match the strings `clientId`, `clientSecret` and `issuerUrl`.

## Step 3: Update cdk.json

In your cdk.json file, add the ID Provider and SecretName to the cdk.json file.

like so:

```json
{
"context": {
// ...
"identityProviders": [
{
"service": "oidc", // Do not change
"serviceName": "<YOUR_SERVICE_NAME>", // Set any value you like
"secretName": "<YOUR_SECRET_NAME>"
}
],
"userPoolDomainPrefix": "<UNIQUE_DOMAIN_PREFIX_FOR_YOUR_USER_POOL>"
}
}
```

### Attention

#### Uniqueness

The `userPoolDomainPrefix` must be globally unique across all Amazon Cognito users. If you choose a prefix that's already in use by another AWS account, the creation of the user pool domain will fail. It's a good practice to include identifiers, project names, or environment names in the prefix to ensure uniqueness.

## Step 4: Deploy Your CDK Stack

Deploy your CDK stack to AWS:

```sh
cdk deploy --require-approval never --all
```

## Step 5: Update OIDC Client with Cognito Redirect URIs

After deploying the stack, `AuthApprovedRedirectURI` is showing on the CloudFormation outputs. Go back to your OIDC configuration and update with the correct redirect URIs.
4 changes: 1 addition & 3 deletions docs/SETUP_IDP.md → docs/idp/SET_UP_GOOGLE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Set up external identity provider

Currently we support Google for external Idp.
# Set up external identity provider for Google

## Step 1: Create a Google OAuth 2.0 Client

Expand Down
4 changes: 1 addition & 3 deletions docs/SET_UP_IDP_ja.md → docs/idp/SET_UP_GOOGLE_ja.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# 外部アイデンティティプロバイダーの設定

現在、外部 Idp として Google をサポートしています。
# 外部アイデンティティプロバイダー (Google) の設定

## ステップ 1: Google OAuth 2.0 クライアントを作成する

Expand Down
Loading

0 comments on commit 5189895

Please sign in to comment.