AWS + Serverless - how to get at the secret key generated by cognito user pool
Asked Answered
H

4

11

I've been following the serverless tutorial at https://serverless-stack.com/chapters/configure-cognito-user-pool-in-serverless.html

I've got the following serverless yaml snippit

Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      # Generate a name based on the stage
      UserPoolName: ${self:custom.stage}-moochless-user-pool
      # Set email as an alias
      UsernameAttributes:
      - email
      AutoVerifiedAttributes:
      - email

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      # Generate an app client name based on the stage
      ClientName: ${self:custom.stage}-user-pool-client
      UserPoolId:
        Ref: CognitoUserPool
      ExplicitAuthFlows:
      - ADMIN_NO_SRP_AUTH
      # >>>>> HOW DO I GET THIS VALUE IN OUTPUT <<<<<
      GenerateSecret: true

# Print out the Id of the User Pool that is created
Outputs:
  UserPoolId:
    Value:
      Ref: CognitoUserPool

  UserPoolClientId:
    Value:
      Ref: CognitoUserPoolClient
  #UserPoolSecret:
  #   WHAT GOES HERE?

I'm exporting all my other config variables to a json file (to be consumed by a mobile app, so I need the secret key).

How do I get the secret key generated to appear in my output list?

Henrietta answered 29/12, 2018 at 7:49 Comment(0)
F
13

The ideal way to retrieve the secret key is to use "CognitoUserPoolClient.ClientSecret" in your cloudformation template.

UserPoolClientIdSecret:
  Value:    
   !GetAtt CognitoUserPoolClient.ClientSecret

But it is not supported as explained here and gives message as shown in the image:Not supported You can run below CLI command to retrieve the secret key as a work around:

aws cognito-idp describe-user-pool-client --user-pool-id "us-west-XXXXXX"  --region us-west-2 --client-id "XXXXXXXXXXXXX" --query 'UserPoolClient.ClientSecret' --output text
Fleshings answered 29/12, 2018 at 11:4 Comment(0)
H
4

As of 2023 it is now possible to fetch the client secret as an Output value in the following manner:

UserPoolClientIdSecret:{
   Value: {
             'Fn::GetAtt': ['CognitoUserPoolClient', 'ClientSecret'],
           }   
} 

Or as YAML:

UserPoolClientIdSecret:
  Value:
    Fn::GetAtt:
      - CognitoUserPoolClient
      - ClientSecret
Harney answered 8/11, 2023 at 21:45 Comment(2)
Even though the AWS documentation does not show the ClientSecret attribute, this definitely works, and should be the accepted answer. Which is funny given that this attribute was previously documented when it did NOT work!Purim
Works flawlessly !! Kudos to @gus-bernardoImpenetrability
B
2

As it is still not possible to get the secret of a Cognito User Pool Client using !GetAtt in a CloudFormation Template I was looking for an alternative solution without manual steps so the infrastructure can get deployed automatically.

I like clav's solution but it requires the Command Runner to be installed first.

So, what I did in the end was using a Lambda-backed custom resource. I wrote it in JavaScript but you can also write it in Python.

Here is an overview of the 3 steps you need to follow:

  1. Create IAM Policy and add it to the Lambda function execution role.
  2. Add creation of In-Line Lambda function to CloudFormation Template.
  3. Add creation of Lambda-backed custom resource to CloudFormation Template.
  4. Get the output from the custom Ressource via !GetAtt

And here are the details:

  1. Create IAM Policy and add it to the Lambda function execution role.
  # IAM: Policy to describe user pool clients of Cognito user pools
  CognitoDescribeUserPoolClientsPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      Description: 'Allows describing Cognito user pool clients.'
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - 'cognito-idp:DescribeUserPoolClient'
            Resource: 
              - !Sub 'arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/*'

If necessary only allow it for certain resources.

  1. Add creation of In-Line Lambda function to CloudFormation Template.
  # Lambda: Function to get the secret of a Cognito User Pool Client
  LambdaFunctionGetCognitoUserPoolClientSecret:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: 'GetCognitoUserPoolClientSecret'
      Description: 'Lambda function to get the secret of a Cognito User Pool Client.'
      Handler: index.lambda_handler
      Role: !Ref LambdaFunctionExecutionRoleArn
      Runtime: nodejs14.x
      Timeout: '30'
      Code:
        ZipFile: |
          // Import required modules
          const response = require('cfn-response');
          const { CognitoIdentityServiceProvider } = require('aws-sdk');

          // FUNCTION: Lambda Handler
          exports.lambda_handler = function(event, context) {
            console.log("Request received:\n" + JSON.stringify(event));

            // Read data from input parameters
            let userPoolId = event.ResourceProperties.UserPoolId;
            let userPoolClientId = event.ResourceProperties.UserPoolClientId;

            // Set physical ID
            let physicalId = `${userPoolId}-${userPoolClientId}-secret`;

            let errorMessage = `Error at getting secret from cognito user pool client:`;
            try {
              let requestType = event.RequestType;
              if(requestType === 'Create') {
                console.log(`Request is of type '${requestType}'. Get secret from cognito user pool client.`);

                // Get secret from cognito user pool client
                let cognitoIdp = new CognitoIdentityServiceProvider();
                cognitoIdp.describeUserPoolClient({
                  UserPoolId: userPoolId,
                  ClientId: userPoolClientId
                }).promise()
                .then(result => {
                  let secret = result.UserPoolClient.ClientSecret;
                  response.send(event, context, response.SUCCESS, {Status: response.SUCCESS, Error: 'No Error', Secret: secret}, physicalId);
                }).catch(error => {
                  // Error
                  console.log(`${errorMessage}:${error}`);
                  response.send(event, context, response.FAILED, {Status: response.FAILED, Error: error}, physicalId);
                });

              } else {
                console.log(`Request is of type '${requestType}'. Not doing anything.`);
                response.send(event, context, response.SUCCESS, {Status: response.SUCCESS, Error: 'No Error'}, physicalId);
              }
            } catch (error){
                // Error
                console.log(`${errorMessage}:${error}`);
                response.send(event, context, response.FAILED, {Status: response.FAILED, Error: error}, physicalId); 
            }
          };

Make sure you pass the right Lambda Execution Role to the parameter Role. It should contain the policy created in step 1.

  1. Add creation of Lambda-backed custom resource to CloudFormation Template.
  # Custom: Cognito user pool client secret
  UserPoolClientSecret:
    Type: Custom::UserPoolClientSecret
    Properties:
      ServiceToken: !Ref LambdaFunctionGetCognitoUserPoolClientSecret
      UserPoolId: !Ref UserPool
      UserPoolClientId: !Ref UserPoolClient

Make sure you pass the Lambda function created in step 2 as ServiceToken. Also make sure you pass in the right values for the parameters UserPoolId and UserPoolClientId. They should be taken from the Cognito User Pool and the Cognito User Pool Client.

  1. Get the output from the custom Ressource via !GetAtt
!GetAtt UserPoolClientSecret.Secret

You can do this anywhere you want.

Blynn answered 25/11, 2022 at 15:20 Comment(0)
U
0

As Prabhakar Reddy points out, currently you can't get the Cognito client secret using !GetAtt in your CloudFormation template. However, there is a way to avoid the manual step of using the AWS command line to get the secret. The AWS Command Runner utility for CloudFormation allows you to run AWS CLI commands from your CloudFormation templates, so you can run the CLI command to get the secret in the CloudFormation template and then use the output of the command elsewhere in your template using !GetAtt. Basically CommandRunner spins up an EC2 instance and runs the command you specify and saves the output of the command to a file on the instance while the CloudFormation template is running so that it can be retrieved later using !GetAtt. Note that CommandRunner is a special custom CloudFormation type that needs to be installed for the AWS account as a separate step. Below is an example CloudFormation template that will get a Cognito client secret and save it to AWS Secrets manager.

Resources:

  CommandRunnerRole:
    Type: AWS::IAM::Role
    Properties:
      # the AssumeRolePolicyDocument specifies which services can assume this role, for CommandRunner this needs to be ec2
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: CommandRunnerPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CancelUploadArchive'
                  - 'logs:GetBranch'
                  - 'logs:GetCommit'
                  - 'cognito-idp:*'
                Resource: '*'

  CommandRunnerInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref CommandRunnerRole

  GetCognitoClientSecretCommand:
    Type: AWSUtility::CloudFormation::CommandRunner
    Properties:
      Command: aws cognito-idp describe-user-pool-client --user-pool-id <user_pool_id> --region us-east-2 --client-id <client_id> --query UserPoolClient.ClientSecret --output text > /command-output.txt
      Role: !Ref CommandRunnerInstanceProfile
      InstanceType: "t2.nano"
      LogGroup: command-runner-logs

  CognitoClientSecret:
    Type: AWS::SecretsManager::Secret
    DependsOn: GetCognitoClientSecretCommand
    Properties:
      Name: "command-runner-secret"
      SecretString: !GetAtt GetCognitoClientSecretCommand.Output

Note that you will need to replace the <user_pool_id> and <client_id> with your user pool and client pool id. A complete CloudFormation template would likely create the Cognito User Pool and User Pool Client and the user pool & client id values could be retrieved from those resources using !Ref as part of a !Join statement that creates the entire command, e.g.

Command: !Join [' ', ['aws cognito-idp describe-user-pool-client --user-pool-id', !Ref CognitoUserPool, '--region', !Ref AWS::Region, '--client-id',  !Ref CognitoUserPoolClient, '--query UserPoolClient.ClientSecret --output text > /command-output.txt']]

One final note, depending on your operating system, the installation/registration of CommandRunner may fail trying to create the S3 bucket it needs. This is because it tries to generate a bucket name using uuidgen and will fail if uuidgen isn't installed. I have opened an issue on the CommandRunner GitHub repo for this. Until the issue is resolved, you can get around this by modifying the /scripts/register.sh script to use a static bucket name.

Unmentionable answered 9/11, 2022 at 19:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.