Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cognito: Using a token for UserPoolIdentityProviderApple doesn't resolve at deploy #31378

Closed
1 task
Someone5328546 opened this issue Sep 9, 2024 · 10 comments · Fixed by #31409
Closed
1 task
Labels
@aws-cdk/aws-cognito Related to Amazon Cognito bug This issue is a bug. effort/medium Medium work item – several days of effort p2

Comments

@Someone5328546
Copy link

Someone5328546 commented Sep 9, 2024

Describe the bug

I'm attempting to create a Cognito user pool using Apple as an identity provider. I created the secret key and manually saved it in Secrets Manager using the AWS console.

Originally, I attempted this:

const applePrivateKeySecret = Secret.fromSecretNameV2(this, 'ApplePrivateKey', 'TheKeyNameInSecretsManager')

const appleProvider = new UserPoolIdentityProviderApple(this, 'UserPoolIdentityProviderApple',
{
    clientId: 'com.myapp',
    teamId: '123456',
    keyId: '123456',
    privateKey: applePrivateKeySecret.secretValue.toString(),
    userPool: this.userPool
})

It didn't work on deploy since the secret is exposed as a string.

I also tried saving the value as a SecureString in AWS Systems Manager and retrieving it like this:

const applePrivateKeyParameter = SecretValue.ssmSecure('AppleSignInPrivateKey').toString()

Which didn't give the same error regarding the value being exposed, but results in a TypeError (described just below), since apparently this token is unresolved when I attempt to deploy.

So I attempted to create a lambda that could be called by my Cognito stack to retrieve the token from Secrets Manager:

The lambda code:

import {
    GetSecretValueCommand,
    SecretsManagerClient
} from '@aws-sdk/client-secrets-manager'

export const handler = async (event: any = {}) : Promise <any> =>
{
    try
    {
        const client = new SecretsManagerClient()
        const command = new GetSecretValueCommand({ SecretId: 'TheKeyNameInSecretsManager' })
        const response = await client.send(command)

        if (response.SecretString)
        {
            return {
                Status: 'SUCCESS',
                PhysicalResourceId: 'ApplePrivateKey',
                Data:
                {
                    SecretValue: response.SecretString
                }
            }
        }
        else
        {
            return {
                Status: 'FAILED',
                Reason: 'SecretString is empty.',
            }
        }
    }
    catch (error: any)
    {
        return {
            status: 'FAILED',
            message: error?.message || 'Unknown Failure.'
        }
    }
}

In my Cognito Stack, I retrieve the token and attempt to use it, but UserPoolIdentityProviderApple is seemingly undefined since the token never resolves.

This line will always throw an error:

this.userPool.registerIdentityProvider(appleProvider)
TypeError: Cannot read properties of undefined (reading 'registerIdentityProvider')

This comment on another issue seems to outline what I'm doing, more or less.

As far as I can tell, aren't tokens supposed to resolve on deployment? But this unresolved token is always causing the above error when I attempt to deploy.

What is the correct way to retrieve a secret from Secrets Manager to use in this context?

The Cognito Stack code:

import {
    AccountRecovery,
    CfnIdentityPool,
    OAuthScope,
    UserPool,
    UserPoolClient,
    UserPoolClientIdentityProvider,
    UserPoolIdentityProviderApple
} from 'aws-cdk-lib/aws-cognito'
import { 
    App,
    CustomResource,
    Stack,
    StackProps,
    Token
} from 'aws-cdk-lib'
import { join } from 'path'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import { Provider } from 'aws-cdk-lib/custom-resources'
import { Runtime } from 'aws-cdk-lib/aws-lambda'
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'



export class CognitoStack extends Stack
{
    public userPool: UserPool
    public userPoolClient: UserPoolClient
    public identityPool: CfnIdentityPool

    constructor(
        scope: App,
        id: string,
        props: StackProps
    )
    {
        super(scope, id, props)



        const applePrivateKeySecret = Secret.fromSecretNameV2(this, 'ApplePrivateKey', 'TheKeyNameInSecretsManager')



        // Create a Lambda to securely retrieve the Apple private key at runtime.
        const getApplePrivateKeyLambda = new NodejsFunction(this, 'GetApplePrivateKey',
        {
            runtime: Runtime.NODEJS_20_X,
            functionName: 'getApplePrivateKey',
            entry: join(__dirname, '..', 'lambda', 'getApplePrivateKey.ts')
        })



        // Grant access to `applePrivateKeySecret`.
        applePrivateKeySecret.grantRead(getApplePrivateKeyLambda)



        // Create a custom resource provider for invoking `getApplePrivateKey`.
        const getApplePrivateKeyProvider = new Provider(this, 'GetApplePrivateKeyProvider',
        {
            onEventHandler: getApplePrivateKeyLambda
        })
    


        // Create a custom resource to call `getApplePrivateKey`.
        const getApplePrivateKeyResource = new CustomResource(this, 'GetApplePrivateKeyResource',
        {
            serviceToken: getApplePrivateKeyProvider.serviceToken
        })



        // Access the secret value directly from the custom resource's attributes.
        const applePrivateKey = getApplePrivateKeyResource.getAtt('ApplePrivateKey').toString()

        console.log(applePrivateKey)
        
        if (typeof applePrivateKey === 'undefined')
        {
            throw new Error('Failed to retrieve Apple private key.');
        }

        

        // Configure Apple as an Identity Provider.
        const appleProvider = new UserPoolIdentityProviderApple(this, 'UserPoolIdentityProviderApple',
        {
            clientId: 'com.myapp',
            teamId: '123456',
            keyId: '123456',
            privateKey: applePrivateKey,
            userPool: this.userPool
        })



        // Create a Cognito User Pool.
        this.userPool = new UserPool(this, 'UserPool',
        {
            signInAliases:
            {
                email: false,
                phone: false,
                preferredUsername: false,
                username: true
            }
        })
    


        // Register Apple as an Identity Provider.
        this.userPool.registerIdentityProvider(appleProvider)



        // Create a User Pool Client.
        this.userPoolClient = new UserPoolClient(this, 'UserPoolClient',
        {
            userPool: this.userPool,
            oAuth:
            {
                flows:
                {
                    authorizationCodeGrant: true
                },
                scopes:
                [
                    OAuthScope.OPENID
                ]
            },
            preventUserExistenceErrors: true,
            supportedIdentityProviders:
            [
                UserPoolClientIdentityProvider.APPLE
            ]
        })



        // Create an Identity Pool for Federated Identities.
        this.identityPool = new CfnIdentityPool(this, 'IdentityPool',
        {
            allowUnauthenticatedIdentities: false,
            cognitoIdentityProviders:
            [
                {
                    clientId: this.userPoolClient.userPoolClientId,
                    providerName: this.userPool.userPoolProviderName,
                    serverSideTokenCheck: true
                }
            ]
        })
    }
}

Regression Issue

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

Last Known Working CDK Version

No response

Expected Behavior

I expected the token to resolve on deployment so I could use it to create an identity provider for Cognito.

Current Behavior

The token does not resolve and breaks the deployment. If I add a check Token.isUnresolved(), it will always be true.

Reproduction Steps

See above code please.

Possible Solution

No response

Additional Information/Context

No response

CDK CLI Version

2.156.0 (build 2966832)

Framework Version

No response

Node.js Version

20.16.0

OS

Windows 10

Language

TypeScript

Language Version

TypeScript 5.5.3

Other information

No response

@Someone5328546 Someone5328546 added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Sep 9, 2024
@github-actions github-actions bot added the @aws-cdk/aws-cognito Related to Amazon Cognito label Sep 9, 2024
@pahud
Copy link
Contributor

pahud commented Sep 10, 2024

According to this:

/**
* The privateKey content for Apple APIs to authenticate the client.
*/
readonly privateKey: string;

It has to be the private_key content which would be used to as Json string for providerdetails. Looking at the provided sample in the CFN document:

"ProviderDetails": { "authorize_scopes": "email name", "client_id": "com.example.cognito", "private_key": "1EXAMPLE", "key_id": "2EXAMPLE", "team_id": "3EXAMPLE" }

Looks like the private_key has to be plain text in the template.

I will reach out internally for clarifying.

@pahud
Copy link
Contributor

pahud commented Sep 10, 2024

internal tracking: V1512372084

@pahud pahud added p2 effort/medium Medium work item – several days of effort and removed needs-triage This issue or PR still needs to be triaged. labels Sep 10, 2024
@pahud
Copy link
Contributor

pahud commented Sep 10, 2024

Hi @Someone5328546

I guess you have three options here but the private key has to be stored in ssm parameter instead:

Let me know if it works for you.

// template parameter with string value(cdk creates a CfnParameter for you)
privateKey: ssm.StringParameter.valueForStringParameter(this, 'foo'),
// template parameter with SSM parameter value(cdk creates a CfnParameter for you)
privateKey: ssm.StringParameter.fromStringParameterAttributes(this, 'privateKey', { parameterName: 'foo', }).stringValue,
// dynamic reference with ssm-secure(cdk creates a dynamic reference for you)
privateKey: ssm.StringParameter. fromSecureStringParameterAttributes(this, 'privateKey', {
   parameterName: 'foo',
   encryptionKey: kms.Key.fromKeyArn(this, 'Key', 'arn:aws:kms:eu-central-1:987654321098:key/abcd1234-ab12-cd34-ef56-abcdef123456'),
 }).stringValue,
// dynamic reference with a secretsmanager Secret(cdk creates a dynamic reference for you)
privateKey: '{{resolve:secretsmanager:secret-id:SecretString:json-key}}'

As the private key is sensitive credentials, it's recommended using fromSecureStringParameterAttributes() or dynamic reference with secret manager secret support.

Check aws-ssm doc for more details or this blog post(SSM Parameters in AWS CDK) for samples.

@pahud pahud added the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Sep 10, 2024
@pahud pahud changed the title aws-cdk: Using a token for UserPoolIdentityProviderApple doesn't resolve at deploy cognito: Using a token for UserPoolIdentityProviderApple doesn't resolve at deploy Sep 10, 2024
@Someone5328546
Copy link
Author

@pahud Thanks for the reply!

So, I tried this option first after saving the value as SecureString in SSM:

privateKey: ssm.StringParameter. fromSecureStringParameterAttributes(this, 'privateKey', {
   parameterName: 'foo',
   encryptionKey: kms.Key.fromKeyArn(this, 'Key', 'arn:aws:kms:eu-central-1:987654321098:key/abcd1234-ab12-cd34-ef56-abcdef123456'),
 }).stringValue

But it threw this error:

Failed to create ChangeSet cdk-deploy-change-set on CognitoStack: FAILED, SSM Secure reference is not supported in: [AWS::Cognito::UserPoolIdentityProvider/Properties/ProviderDetails/private_key]

So then I saved the value as a String in SSM, and tried this option:

privateKey: ssm.StringParameter.valueForStringParameter(this, 'foo')

This didn't throw the same error, but it did throw this error:

CREATE_FAILED | AWS::Cognito::UserPoolClient | UserPoolClient (UserPoolClient) Resource handler returned message: "The provider SignInWithApple does not exist for User Pool region_id (Service: CognitoIdentityProvider, Status Code: 400, Request ID: ...)" (RequestToken: ..., HandlerErrorCode: InvalidRequest)

I haven't used AWS CDK before, but it seems like I covered the bases. Do you know what went wrong?

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Sep 10, 2024
@Someone5328546
Copy link
Author

Someone5328546 commented Sep 10, 2024

I found this issue with a solution that worked.

UserPoolClient.node.addDependency(provider)

CDK can't handle this automatically because the UserPoolClient only receives the name of the provider (provider.providerName) as a string. This name is not sufficient to generate the dependency between both constructs.

Now it would be great to be able to use the SecureString parameter instead of just the regular String parameter, if possible. Unfortunately, based on this documentation, it seems like it might not be widely supported.

If it's not possible, could you please shed some light on what some of the potential drawbacks to not using the SecureString might be? Is my Sign in with Apple private key at risk of exposure since its not in a SecureString or Secret? Thanks!

@pahud
Copy link
Contributor

pahud commented Sep 10, 2024

Yes according to "Resources that support dynamic parameter patterns for secure strings" described in the doc, only a few services support that. I have cut an internal ticket to bring this to relevant team's attention. Unfortunately there isn't anything CDK can do as that is defined in CFN spec. I will report here when I have updates.

@pahud
Copy link
Contributor

pahud commented Sep 11, 2024

OK please check out this sample below:

Let's say you have a Secrets Manager Secret called "TheKeyNameInSecretsManager" with its secret value as JSON and "private_key" in the JSON payload as below:

% aws secretsmanager get-secret-value --secret-id TheKeyNameInSecretsManager --query "SecretString"
"{\"private_key\":\"foo\"}"

Now, our goal is to build a dynamic reference with secretsmanager as the service like

private_key: "{{resolve:secretsmanager:TheKeyNameInSecretsManager:SecretString:private_key::}}"

Your CDK code should look like this:

    const applePrivateKeySecret = secretsmanager.Secret.fromSecretNameV2(this, 'ApplePrivateKey', 'TheKeyNameInSecretsManager')

    // create a random userpool
    const userPool = new cognito.UserPool(this, 'UserPool', { })
    new cognito.UserPoolIdentityProviderApple(this, 'UserPoolIdentityProviderApple',
    {
        clientId: 'com.myapp',
        teamId: '123456',
        keyId: '123456',
        privateKey: SecretValue.secretsManager(applePrivateKeySecret.secretName, {
          jsonField: 'private_key',
        }).unsafeUnwrap().toString(),
        userPool,
    })

You can cdk synth and validate the output as below:

 UserPoolIdentityProviderApple94455E07:
    Type: AWS::Cognito::UserPoolIdentityProvider
    Properties:
      ProviderDetails:
        client_id: com.myapp
        team_id: "123456"
        key_id: "123456"
        private_key: "{{resolve:secretsmanager:TheKeyNameInSecretsManager:SecretString:private_key::}}"
        authorize_scopes: name
      ProviderName: SignInWithApple
      ProviderType: SignInWithApple
      UserPoolId:
        Ref: UserPool6BA7E5F2

In this case, your private_key would be stored in the secretsmanager Secret and cloudformation would reference it using dynamic references. I didn't really deploy it as I don't have a valid private_key but cdk synth was successful.

Let me know if it works for you.

@pahud
Copy link
Contributor

pahud commented Sep 11, 2024

I think we need a PR to fix this.

Just like clientSecretValue for Google IdP which is typed SecretValue

readonly clientSecretValue?: SecretValue;

I think this has to be SecretValue as well

Copy link

Comments on closed issues and PRs are hard for our team to see.
If you need help, please open a new issue that references this one.

1 similar comment
Copy link

Comments on closed issues and PRs are hard for our team to see.
If you need help, please open a new issue that references this one.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 13, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
@aws-cdk/aws-cognito Related to Amazon Cognito bug This issue is a bug. effort/medium Medium work item – several days of effort p2
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants