Skip to content

aws-cognito: Creating user pool with custom attributes #34379

Closed
@codefromrvk

Description

@codefromrvk

Describe the bug

When creating the cognito user pool with many custom attributes . I get the below error-
Invalid read attributes specified while creating a client .

My guess is because of eventual consistency. Is there anything wrong in below code that is stooping it from adding all the cusotm attributes at once?

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Library Version

2.1001.0

Expected Behavior

Create all custom attributes at once when user pool is created

Current Behavior

Errors out when there are multiple custom attributes

Image

Reproduction Steps

This my stack and If add more custom attributes it will error out.

import { StandardAttributesMask } from 'aws-cdk-lib/aws-cognito/lib/user-pool-attr.d';
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as cdk from 'aws-cdk-lib';
import * as certificatemanager from 'aws-cdk-lib/aws-certificatemanager';
import * as secretsManager from 'aws-cdk-lib/aws-secretsmanager';

import {
  CfnUserPoolResourceServer,
  OidcAttributeRequestMethod,
  ProviderAttribute,
  UserPoolIdentityProviderOidc,
} from 'aws-cdk-lib/aws-cognito';

export interface OidcProviderProps {
  name: string;
  secretKey: string;
  secretKeyLookup?: string;
  authorizationUrl?: boolean;
  tokenUrl?: boolean;
  userInfoUrl?: boolean;
  jwksUriUrl?: boolean;
}

export interface CognitoUserPoolProps {
  userPoolName: string;
  domainName: string;
  certificateArn: string;
  selfSignupEnabled: boolean;
  ssoApplicationOidcProviders: OidcProviderProps[];
  ssoAppCallbackUrls: string[] | undefined;
  ssoAppLogoutUrl: string[] | undefined;
}

const customAttributes = [
  'empId',
  'orgId',
];

//  User Pool Client attributes
const standardCognitoAttributes: StandardAttributesMask = {
  givenName: true,
  familyName: true,
  email: true,
  emailVerified: true,
  middleName: true,
  fullname: true,
  nickname: true,
  phoneNumber: true,
  phoneNumberVerified: true,
  profilePicture: true,
  preferredUsername: true,
  timezone: true,
  lastUpdateTime: true,
};

// Note: When creating the user pool for the first time , with many custom attributes, it might lead to an error - `Invalid read attributes specified while creating a client . To overcome this error, create the user pool with less custom attributes and then add the custom attributes later.`

export class CognitoOIDCCoreStack extends cdk.Stack {
  id!: string;
  constructor(scope: cdk.App, id: string, props: CognitoUserPoolProps) {
    super(scope, id);

    this.id = id

    // User Pool
    const userPool = new cognito.UserPool(this, `universe-userpool-${id}`, {
      userPoolName: props.userPoolName,
      selfSignUpEnabled: props.selfSignupEnabled,
      signInCaseSensitive: false,
      signInAliases: {
        email: true,
      },
      autoVerify: {
        email: true,
      },
      standardAttributes: {
        email: {
          required: true,
          mutable: true,
        },
        givenName: {
          required: false,
          mutable: true,
        },
        familyName: {
          required: false,
          mutable: true,
        },
      },

      customAttributes: {
        empId: new cognito.StringAttribute({ mutable: true }),
        orgId: new cognito.StringAttribute({ mutable: true }),
       // Errors out if I create the user pool with more attributes
      },
      passwordPolicy: {
        minLength: 8,
        requireLowercase: true,
        requireDigits: true,
        requireUppercase: true,
        requireSymbols: true,
      },
      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    userPool.addDomain('cognito-domain', {
      customDomain: {
        domainName: props.domainName,
        certificate: certificatemanager.Certificate.fromCertificateArn(
          this,
          'cloudfrontCert',
          props.certificateArn,
        ),
      },
    });

    new CfnUserPoolResourceServer(this, 'universe-userpool-resource-server', {
      identifier: 'https://universe.io/',
      name: `universe-resource-server-${id}`,
      userPoolId: userPool.userPoolId,
      scopes: [
        {
          scopeDescription: 'universe Admin with read permissions',
          scopeName: 'ublox.ad.read',
        },
        {
          scopeDescription: 'universe Admin with read write permissions',
          scopeName: 'ublox.ad.readwrite',
        },
        {
          scopeDescription: 'Tenant product permissions',
          scopeName: 'product.read',
        },
      ],
    });

    this.createSSOApplicationClient(
      userPool,
      standardCognitoAttributes,
      props.ssoAppCallbackUrls,
      props.ssoAppLogoutUrl,
      props.ssoApplicationOidcProviders,
    );

    // Outputs
    new cdk.CfnOutput(this, 'userPoolId', {
      value: userPool.userPoolId,
    });
  }

  createSSOApplicationClient(
    userPool: cognito.UserPool,
    standardCognitoAttributes: StandardAttributesMask,
    callbackUrls?: string[],
    logoutUrls?: string[],
    oidcProvidersConfig?: OidcProviderProps[],
  ) {
    const oidcProvider: UserPoolIdentityProviderOidc[] = [];
    const oidcProviders: cognito.UserPoolClientIdentityProvider[] = [];

    const clientReadAttributes = new cognito.ClientAttributes()
      .withStandardAttributes(standardCognitoAttributes)
      .withCustomAttributes(...customAttributes);

    const clientWriteAttributes = new cognito.ClientAttributes()
      .withStandardAttributes({
        ...standardCognitoAttributes,
        emailVerified: false,
        phoneNumberVerified: false,
      })
      .withCustomAttributes(...customAttributes);

    if (oidcProvidersConfig !== undefined) {
      for (let i = 0; i < oidcProvidersConfig.length; i += 1) {
        const oidcProviderConfig = oidcProvidersConfig[i];
        const oidcSecretKey = secretsManager.Secret.fromSecretNameV2(
          this,
          `OidcProviderSecret${i}`,
          oidcProviderConfig.secretKey,
        );

        const clientId =
          oidcSecretKey.secretValueFromJson('clientId')?.toString() ??
          undefined;
        const clientSecret =
          oidcSecretKey.secretValueFromJson('clientSecret').toString() ??
          undefined;
        const issuerUrl =
          oidcSecretKey.secretValueFromJson('issuerUrl')?.toString() ??
          undefined;
        const authorization = oidcProviderConfig.authorizationUrl
          ? oidcSecretKey.secretValueFromJson('authorizationUrl').toString()
          : undefined;
        const token = oidcProviderConfig.tokenUrl
          ? oidcSecretKey.secretValueFromJson('tokenUrl')?.toString()
          : undefined;
        const userInfo = oidcProviderConfig.userInfoUrl
          ? oidcSecretKey.secretValueFromJson('userInfoUrl')?.toString()
          : undefined;
        const jwksUri = oidcProviderConfig.jwksUriUrl
          ? oidcSecretKey.secretValueFromJson('jwksUriUrl')?.toString()
          : undefined;
        const identityProviderOidc = new UserPoolIdentityProviderOidc(
          this,
          oidcProviderConfig.name,
          {
            userPool,
            name: oidcProviderConfig.name,
            clientId,
            clientSecret,
            issuerUrl,
            ...(authorization &&
              token &&
              userInfo &&
              jwksUri && {
              endpoints: {
                authorization,
                token,
                userInfo,
                jwksUri,
              },
            }),
            scopes: ['profile', 'email', 'openid'],
            attributeRequestMethod: OidcAttributeRequestMethod.GET,
            attributeMapping: {
              email: ProviderAttribute.other('email'),
              custom: {
                'custom:empId': ProviderAttribute.other('EMP_ID'),
                'custom:orgId': ProviderAttribute.other('ORG_ID'),
              },
            },
          },
        );

        oidcProvider.push(identityProviderOidc);
        oidcProviders.push(
          cognito.UserPoolClientIdentityProvider.custom(
            identityProviderOidc.providerName,
          ),
        );
      }
    }

    // User Pool Client
    const userPoolClient = new cognito.UserPoolClient(
      this,
      'sso-application-cognito-client',
      {
        userPool,
        authFlows: {
          adminUserPassword: true,
          custom: true,
          userSrp: true,
        },
        supportedIdentityProviders: [
          cognito.UserPoolClientIdentityProvider.COGNITO,
          ...oidcProviders,
        ],
        readAttributes: clientReadAttributes,
        writeAttributes: clientWriteAttributes,
        oAuth: {
          callbackUrls,
          logoutUrls,
          scopes: [
            cognito.OAuthScope.OPENID,
            cognito.OAuthScope.EMAIL,
            cognito.OAuthScope.PHONE,
            cognito.OAuthScope.PROFILE,
            cognito.OAuthScope.custom('aws.cognito.signin.user.admin'),
          ],
          flows: {
            authorizationCodeGrant: true,
            implicitCodeGrant: true,
          },
        },
        userPoolClientName: `universe-SSO-Application-Client-${this.id}`,
      },
    );

    userPoolClient.node.addDependency(...oidcProvider);

    // Outputs
    new cdk.CfnOutput(this, 'ssoApplicationUserPoolClient', {
      value: userPoolClient.userPoolClientId,
    });
  }
}

Possible Solution

Might be because of eventual consistency. But shouldnt happen

Additional Information/Context

Running the CDK in github action

AWS CDK Library version (aws-cdk-lib)

2.181.1

AWS CDK CLI version

2.1001.0

Node.js Version

22.11.0

OS

macos

Language

TypeScript

Language Version

4.9.5

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    @aws-cdk/aws-cognitoRelated to Amazon CognitobugThis issue is a bug.closed-for-stalenessThis issue was automatically closed because it hadn't received any attention in a while.effort/mediumMedium work item – several days of effortp3response-requestedWaiting on additional info and feedback. Will move to "closing-soon" in 7 days.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions