simonmcallister0210 / cognito-srp-helper

A helper for SRP authentication in AWS Cognito
Apache License 2.0
12 stars 3 forks source link
aws aws-cognito cognito helper srp typescript

🔐 Cognito SRP Helper

JavaScript helper used to calculate the values required for SRP authentication in AWS Cognito

If you've ever tried to use the in-built SRP authentication flows in Cognito (USER_SRP_AUTH or CUSTOM_AUTH) using initiateAuth and respondToAuthChallenge, you may have encountered holes in the documentation that don't explain specific fields (SRP_A, TIMESTAMP, PASSWORD_CLAIM_SIGNATURE). You may also notice that there are no SDK functions that will generate values for these fields, leaving you stuck and unable to progress. This helper was created to bridge the missing support for SRP authentication in AWS Cognito, providing functions that will handle the necessary calculations needed to complete the authentication flow

The helper works by providing functions that generate the required hashes for your secret and password, and wrapping your Cognito request and returning the same request with the required SRP fields. It work's with AWS SDK v2 and v3

Usage

This is a Hybrid package, so you can use both ES import:

import CognitoSrpHelper from "cognito-srp-helper";

Or CommonJS require:

const CognitoSrpHelper = require("cognito-srp-helper");

Here is an example of how you would use the helper to implement SRP authentication with Cognito using the AWS JavaScript SDK v3:

// . . . obtain user credentials, IDs, and setup Cognito client

const secretHash = createSecretHash(username, clientId, secretId);
const srpSession = createSrpSession(username, password, poolId, false);

const initiateAuthRes = await cognitoIdentityProviderClient.send(
  new InitiateAuthCommand(
    wrapInitiateAuth(srpSession, {
      ClientId: clientId,
      AuthFlow: "USER_SRP_AUTH",
      AuthParameters: {
        CHALLENGE_NAME: "SRP_A",
        SECRET_HASH: secretHash,
        USERNAME: username,
      },
    }),
  ),
);

const signedSrpSession = signSrpSession(srpSession, initiateAuthRes);

const respondToAuthChallengeRes = await cognitoIdentityProviderClient.send(
  new RespondToAuthChallengeCommand(
    wrapAuthChallenge(signedSrpSession, {
      ClientId: clientId,
      ChallengeName: "PASSWORD_VERIFIER",
      ChallengeResponses: {
        SECRET_HASH: secretHash,
        USERNAME: username,
      },
    }),
  ),
);

// . . . return login tokens from respondToAuthChallengeRes

Here is an example of how you would use createDeviceVerifier to confirm a device:

// Calculate device verifier and a random password using the device and group key

const { DeviceGroupKey, DeviceKey } = respondToAuthChallengeResponse.AuthenticationResult.NewDeviceMetadata;
const { DeviceSecretVerifierConfig, DeviceRandomPassword } = createDeviceVerifier(DeviceKey, DeviceGroupKey);

await cognitoIdentityProviderClient.send(
  new ConfirmDeviceCommand({
    AccessToken,
    DeviceKey,
    DeviceName: "example-friendly-name", // usually this is set a User-Agent
    DeviceSecretVerifierConfig,
  }),
);

Here is an exampe of how you would use signSrpSessionWithDevice to complete signin with a device. Remember you need DeviceKey to complete authentication with a device, so store in on your initial signin attempt before it's required for subsequent authentication attempts with a device. DeviceGroupKey can be obtained from RespondToAuthChallenge responses:

// . . . obtain user credentials, IDs, and setup Cognito client

// Initiate signin with username and password

const srpSession = createSrpSession(username, password, poolId, false);

const initiateAuthRes = await cognitoIdentityProviderClient.send(
  new InitiateAuthCommand(
    wrapInitiateAuth(srpSession, {
      ClientId: clientId,
      AuthFlow: "USER_SRP_AUTH",
      AuthParameters: {
        CHALLENGE_NAME: "SRP_A",
        SECRET_HASH: secretHash,
        USERNAME: username,
        DEVICE_KEY: DeviceKey, // Fetch this from client storage
      },
    }),
  ),
);

// Respond to PASSWORD_VERIFIER challenge

const signedSrpSession = signSrpSession(srpSession, initiateAuthRes);

const respondToAuthChallengeRes1 = await cognitoIdentityProviderClient.send(
  new RespondToAuthChallengeCommand(
    wrapAuthChallenge(signedSrpSession, {
      ClientId: clientId,
      ChallengeName: "PASSWORD_VERIFIER",
      ChallengeResponses: {
        SECRET_HASH: secretHash,
        USERNAME: username,
        DEVICE_KEY: DeviceKey,
      },
      Session: initiateAuthRes.Session,
    }),
  ),
);

// Respond to DEVICE_SRP_AUTH challenge

const respondToAuthChallengeRes2 = await cognitoIdentityProviderClient.send(
  new RespondToAuthChallengeCommand(
    wrapAuthChallenge(signedSrpSession, {
      ClientId: clientId,
      ChallengeName: "DEVICE_SRP_AUTH",
      ChallengeResponses: {
        SECRET_HASH: secretHash,
        USERNAME: username,
        DEVICE_KEY: DeviceKey,
      },
      Session: respondToAuthChallengeRes1.Session,
    }),
  ),
);

// Respond to DEVICE_PASSWORD_VERIFIER challenge

const signedSrpSessionWithDevice = signSrpSessionWithDevice(
  srpSession,
  respondToAuthChallengeRes2,
  DeviceGroupKey,
  DeviceRandomPassword,
);

const respondToAuthChallengeRes3 = await cognitoIdentityProviderClient.send(
  new RespondToAuthChallengeCommand(
    wrapAuthChallenge(signedSrpSessionWithDevice, {
      ClientId: clientId,
      ChallengeName: "DEVICE_PASSWORD_VERIFIER",
      ChallengeResponses: {
        SECRET_HASH: secretHash,
        USERNAME: username,
        DEVICE_KEY: DeviceKey,
      },
      Session: respondToAuthChallengeRes2.Session,
    }),
  ),
);

// . . . return login tokens from respondToAuthChallengeRes3

API

The types InitiateAuthRequest, InitiateAuthResponse, RespondToAuthChallengeRequest refer to both the SDK v2 and v3 versions of these types, and their admin variants. For example InitiateAuthRequest can be AdminInitiateAuthRequest, InitiateAuthCommandInput, etc.

createSecretHash

Generates the required secret hash when a secret is configured for the app client

Parameters

username - string - The user's username

clientId - string - The client ID for the Cognito app

secretId - string - The secret ID for the Cognito app

Returns:

string - A hash of the secret. This is passed to the SECRET_HASH field


createPasswordHash

Generates the required password hash from the user's credentials and user pool ID

NOTE: pre-hashing the password only works when you're sign-in attribute is Username. If you're using Email or Phone Number you need to use an unhashed password

Parameters:

username - string - The user's username

password - string - The user's password

poolId - string - The ID of the user pool the user's credentials are stored in

Returns:

string - A hash of the user's password. Used to create an SRP session


createSrpSession

Creates an SRP session using the user's credentials and a Cognito user pool ID. This session contains the public/private SRP key for the client, and a timestamp in the unique format required by Cognito. With this session we can add to our public key (SRP_A) to the initiateAuth request

NOTE: pre-hashing the password only works when you're sign-in attribute is Username. If you're using Email or Phone Number you should set isHashed as false

username - string - The user's username

password - string - The user's password

poolId - string - The ID of the user pool the user's credentials are stored in

isHashed - boolean - A flag indicating whether the password has already been hashed. The default value is true

Returns:

SrpSession - Client SRP session object containing user credentials and session keys


createDeviceVerifier

When you confirm a device with ConfirmDeviceCommand you need to pass in DeviceSecretVerifierConfig. You can get this value from this function. The function will also generate a unique password DeviceRandomPassword which you will need to authenticate the device in future DEVICE_SRP_AUTH flows

deviceKey - string - The device unique key returned from a RespondToAuthChallengeResponse

deviceGroupKey - string - The device group key returned from a RespondToAuthChallengeResponse

Returns:

DeviceVerifier - An object containing DeviceRandomPassword, PasswordVerifier, and Salt. Used for device verification and authentication


signSrpSession

With a successful initiateAuth call using the USER_SRP_AUTH flow (or CUSTOM_AUTH if SRP is configured) we receive values from Cognito that we can use to verify the user's password. With this response we can 'sign' our session by generating a password signature and attaching it to our session

Parameters:

session - SrpSession - Client SRP session object containing user credentials and session keys

response - InitiateAuthResponse - The Cognito response from initiateAuth. This response contains SRP values (SRP_B, SALT, SECRET_BLOCK) which are used to verify the user's password

Returns:

SrpSessionSigned - A signed version of the SRP session object


signSrpSessionWithDevice

When responding to a DEVICE_SRP_AUTH challenge, you need to sign the SRP session with a device using this function. With a RespondToAuthChallenge response we can 'sign' our session by generating a password signature and attaching it to our session

Parameters:

session - SrpSession - Client SRP session object containing user credentials and session keys

response - RespondToAuthChallengeResponse - The Cognito response from initiateAuth. This response contains SRP values (SRP_B, SALT, SECRET_BLOCK, and DEVICE_KEY when authenticating a device) which are used to verify the user's password

deviceGroupKey - string - The device group key

deviceRandomPassword - string - The random password generated by createDeviceVerifier

Returns:

SrpSessionSigned - A signed version of the SRP session object


wrapInitiateAuth

Wraps a InitiateAuthRequest and attaches the SRP_A field required to initiate SRP

Parameters:

session - SrpSession - SRP session object containing user credentials and session keys

request - InitiateAuthRequest - The Cognito request passed into initiateAuth

Returns:

InitiateAuthRequest - The same request but with the additional SRP_A field


wrapAuthChallenge

Wraps a RespondToAuthChallengeRequest and attaches the PASSWORD_CLAIM_SECRET_BLOCK, PASSWORD_CLAIM_SIGNATURE, and TIMESTAMP fields required to complete SRP

Parameters:

session - SrpSessionSigned - A signed version of the SRP session object

request - RespondToAuthChallengeRequest - The Cognito request passed into respondToAuthChallenge

Returns:

RespondToAuthChallengeRequest - The same request but with the additional PASSWORD_CLAIM_SECRET_BLOCK, PASSWORD_CLAIM_SIGNATURE, and TIMESTAMP fields

Password hashing

It's possible to hash the user's password before you create the SRP session. This might be useful if you're calling InitiateAuth from the backend. This step can add an extra layer of security by obfuscating the user's password. To be clear though, the user's password is perfectly secure being transmitted using a secure protocol like HTTPS, this step is entirely optional

Be aware that password hashing will only work if the user's sign-in attribute is Username. If you're using Email or Phone Number the hashing function createPasswordHash will not generate a valid hash

Zero values in SRP

Should you worry about 0 being used during the SRP calculations?

Short answer: no!

Long answer: according to the safeguards of SRP, if a 0 value is given for A, B, or u then the protocol must abort to avoid compromising the security of the exchange. The possible scenarios in which a 0 value is used are:

  1. A value of 0 is randomly generated via SHA256 which is extremely unlikely to occur, ~1/10^77
  2. A SRP_B value of 0 is received from the Cogntio initiateAuth call, which won't happen unless someone is purposefully trying to compromise security by intercepting the response from Cognito

If any of these scenarios occur this package will throw a AbortOnZeroSrpError, so you don't need to worry about the security of the exchange being compromised

See Also