1
votes

I am working on a serverless project using node.js and AWS Lambda. For auth, I am using AWS Cognito. (Frontend is a web-app in Vue.js on AWS Amplify).

I would like to write my own implementation of resetting a user's password who has forgotten their password.

Basically, the end-user fills up a form with their email. If email is in the system, I send them a reset link (which has a unique code I set in the DB).

I am aware of Cognito's Forgot Password flow and also a solution in which I can capture Cognito's "email sending" code and over-ride the email with my own template passing the code in the URL mentioned here.

I stumbled upon the adminSetUserPassword API which I was sure would work -- but no matter what I do, my lambda function does not get permissions to execute this operation.

This is my nodejs code:

import AWS from 'aws-sdk';
const COGNITO_POOL_ID = process.env.COGNITO_USERPOOL_ID;

const csp = new AWS.CognitoIdentityServiceProvider();

export async function resetUserPassword(username, newPassword) {
  // Constructing request to send to Cognito
  const params = {
    Password: newPassword,
    UserPoolId: COGNITO_POOL_ID,
    Username: username,
    Permanent: true,
  };

  await csp.adminSetUserPassword(params).promise();
  return true;
}

This is my IAM permission for the lambda function (it is in serverless yml format):

CognitoResetPasswordIAM:
  Effect: Allow
  Action:
    - cognito-idp:*
  Resource:
    - arn:aws:cognito-idp:us-east-1::*

(I will fine-tune the permissions once this works)

The following is the error message I am getting. I am starting to feel that my approach to doing this is not the recommended way of doing things.

User: arn:aws:sts::[XXXXXXX]:assumed-role/[YYYYYYYYY]-us-east-1-lambdaRole/web-app-service-dev-resetPassword is not authorized to perform: cognito-idp:AdminSetUserPassword on resource: arn:aws:cognito-idp:us-east-1:[[XXXXXXX]]:userpool/us-east-1_ZZZZZZZZ

(Serverless has access to my AWS Access key with * permissions on * resources -- so I don't think I am missing any permissions there).

My questions:

  • Is this the recommended way of doing this?
  • Is it possible for me to configure permissions in a way that my lambda functions have the required permissions to perform this operation?
1
have you got any solution?Sushant Somani
@SushantSomani I have shared my solution below. Thanks for checking. I forgot I had asked on Stack Overflow. Let me know if you have any queries.saurabhj

1 Answers

1
votes

It turns out, you need to use the Amplify API and not the Cognito API. This involves a couple of steps:

1. Configure your Cognito Amplify Service for Auth.

import Amplify, { Auth } from 'aws-amplify';

export function configureCognitoAuth() {
  Amplify.configure({
    Auth: {
      region: process.env.COGNITO_REGION,
      userPoolId: process.env.COGNITO_USERPOOL_ID,
      mandatorySignIn: false,
      userPoolWebClientId: process.env.COGNITO_CLIENT_ID,
      authenticationFlowType: 'USER_PASSWORD_AUTH',
      oauth: {
        domain: process.env.COGNITO_APP_DOMAIN,
        scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
        responseType: 'code', // or 'token', note that REFRESH token will only be generated when the responseType is code
      },
    },
  });

  // You can get the current config object
  Auth.configure();
}

2. Call the Auth.forgotPassword service to send the actual password here

import { Auth } from 'aws-amplify';

async function sendUserPasswordResetEmail(event) {
  // Any validation checks, rate limits you want to check here, etc.

  try {
    configureCognitoAuth();
    await Auth.forgotPassword(userId);
  } catch (error) {
    // An error occurred while sending the password reset email
  }
}

3. Write a forgotPasswordEmailTrigger Cognito Hook This replaces the default Cognito Reset password email with your own custom email.

This is also a lamdba method which you need to attach to the Cognito Custom Message trigger (from Cognito > General Settings > Triggers)

Custom Message Trigger

My code for this looks like so:

async function forgotPasswordEmailTrigger(event, context, callback) {
  // Confirm it is a PreSignupTrigger
  if (event.triggerSource === 'CustomMessage_ForgotPassword') {
    const { userName } = event;
    const passwordCode = event.request.codeParameter;
    const resetUrl = `${BASE_URL}/password_reset/${userName}/${passwordCode}`;

    let message = 'Your HTML email template goes here';
    message = message
      .replace(/{{passwordResetLink}}/g, resetUrl);

    event.response.emailSubject = 'Email Subject here';
    event.response.emailMessage = message;
  }

  // Return to Amazon Cognito
  callback(null, event);
}

The event.request.codeParameter is where the code is returned from Cognito. I think there is a way to change this, but I didn't bother. I use the same code to verify in the next step.

4. Call the forgotPasswordSubmit method from the Amplify Auth service when a password reset request is sent to your backend

When the user clicks the URL, they come to the website and I pick up the code and the userID from the URL (from Step 3) and then verify the code + reset the password like so:

async function resetPassword(event) {
  const { token, password, user_id } = event.body;

  // Do your validations & checks

  // Getting to here means everything is in order. Reset the password
  try {
    configureCognitoAuth(); // See step 1
    await Auth.forgotPasswordSubmit(user_id, token, password);
  } catch (error) {
    // Error occurred while resetting the password
  }

  const result = {
    result: true,
  };

  return {
    statusCode: 200,
    body: JSON.stringify(result),
  };
}