From 9d56963baf744916401b8b14d242696d92c96939 Mon Sep 17 00:00:00 2001 From: Simon McAllister <56324559+simonmcallister0210@users.noreply.github.com> Date: Tue, 26 Mar 2024 01:34:11 +0000 Subject: [PATCH] feat: allow users to defer password hasing with `isHashed` parameter in `createSrpSession` (#29) * add isHashed flag to createSrpSession to allow users to defer password hashing to signSrpSession * fix integration test * update tests * add node v21 to test since it's in active use * fix incorrectly referenced env var * ban 0 for short public key test cases to stop AbortOnZeroSrpError causing flaking tests * update integration tests * update test workflow with new github action secrets * update github actions node version * update github actions node version * drop support for node v16 * only run integration tests after unit tests * drop tests for node v21 for now * only run integration tests if unit tests pass * update API docs in README. add section about password hashing * rm node v21 for release workflow * rm comment * update README examples * update CONTRIBUTING * typo --------- Co-authored-by: Simon McAllister Co-authored-by: Simon McAllister --- .github/workflows/release.yml | 6 +- .github/workflows/test.yml | 42 +++- CONTRIBUTING.md | 60 +---- README.md | 75 +++---- src/__tests__/integration/sdk-v2.test.ts | 211 +++++++++++++++--- src/__tests__/integration/sdk-v3.test.ts | 207 ++++++++++++++--- src/__tests__/mocks/data.ts | 45 ++-- src/__tests__/mocks/index.ts | 1 + src/__tests__/test-cases/credentials.ts | 3 + .../test-cases/initiate-auth-response.ts | 3 +- .../test-cases/srp-sessions-signed.ts | 18 +- src/__tests__/test-cases/srp-sessions.ts | 15 +- .../unit/create-password-hash.test.ts | 13 +- src/__tests__/unit/create-secret-hash.test.ts | 13 +- src/__tests__/unit/create-srp-session.test.ts | 31 ++- src/__tests__/unit/sign-srp-session.test.ts | 16 +- src/cognito-srp-helper.ts | 28 ++- src/types.ts | 11 +- 18 files changed, 565 insertions(+), 233 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cf21ab..0716908 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,14 +10,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # https://what-version-of-node.js.org/ - node-version: [16.x, 18.x, 20.x] # remember to update this when support is added/dropped + # https://nodejs.org/en/about/previous-releases#release-schedule + node-version: [18, 20] # remember to update this when support is added/dropped steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: - node-version: 18 # remember to update this when support is added/dropped + node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint - run: npm run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a84470e..6f986c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,24 +11,42 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # https://what-version-of-node.js.org/ - node-version: [16.x, 18.x, 20.x] # remember to update this when support is added/dropped + # https://nodejs.org/en/about/previous-releases#release-schedule + node-version: [18, 20] # remember to update this when support is added/dropped steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: - node-version: 18 # remember to update this when support is added/dropped + node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint - run: npm run build - - run: npm run test + - run: npm run test:unit + - run: npm run test:integration env: - AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} - AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} - INT_TEST_USERNAME: ${{secrets.INT_TEST_USERNAME}} - INT_TEST_PASSWORD: ${{secrets.INT_TEST_PASSWORD}} - INT_TEST_POOL_ID: ${{secrets.INT_TEST_POOL_ID}} - INT_TEST_CLIENT_ID: ${{secrets.INT_TEST_CLIENT_ID}} - INT_TEST_SECRET_ID: ${{secrets.INT_TEST_SECRET_ID}} - INT_TEST_AWS_REGION: ${{secrets.INT_TEST_AWS_REGION}} + # Credentials + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + # Username + INT_TEST__USERNAME__USERNAME: ${{ secrets.INT_TEST__USERNAME__USERNAME }} + INT_TEST__USERNAME__PASSWORD: ${{ secrets.INT_TEST__USERNAME__PASSWORD }} + INT_TEST__USERNAME__POOL_ID: ${{ secrets.INT_TEST__USERNAME__POOL_ID }} + INT_TEST__USERNAME__CLIENT_ID: ${{ secrets.INT_TEST__USERNAME__CLIENT_ID }} + INT_TEST__USERNAME__SECRET_ID: ${{ secrets.INT_TEST__USERNAME__SECRET_ID }} + + # Email + INT_TEST__EMAIL__USERNAME: ${{ secrets.INT_TEST__EMAIL__USERNAME }} + INT_TEST__EMAIL__PASSWORD: ${{ secrets.INT_TEST__EMAIL__PASSWORD }} + INT_TEST__EMAIL__POOL_ID: ${{ secrets.INT_TEST__EMAIL__POOL_ID }} + INT_TEST__EMAIL__CLIENT_ID: ${{ secrets.INT_TEST__EMAIL__CLIENT_ID }} + INT_TEST__EMAIL__SECRET_ID: ${{ secrets.INT_TEST__EMAIL__SECRET_ID }} + + # Phone + INT_TEST__PHONE__USERNAME: ${{ secrets.INT_TEST__PHONE__USERNAME }} + INT_TEST__PHONE__PASSWORD: ${{ secrets.INT_TEST__PHONE__PASSWORD }} + INT_TEST__PHONE__POOL_ID: ${{ secrets.INT_TEST__PHONE__POOL_ID }} + INT_TEST__PHONE__CLIENT_ID: ${{ secrets.INT_TEST__PHONE__CLIENT_ID }} + INT_TEST__PHONE__SECRET_ID: ${{ secrets.INT_TEST__PHONE__SECRET_ID }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf05335..3fad872 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,13 +5,11 @@ Table of Contents: 1. [Steps for Implementing a Fix or Feature](#steps-for-implementing-a-fix-or-feature) 2. [Conventional Commit](#conventional-commit) 3. [Releases](#releases) -4. [VSCode setup](#vscode-setup) -5. [Running integration tests locally](#running-integration-tests-locally) -6. [Supporting old major releases](#supporting-old-major-releases) +4. [Running integration tests locally](#running-integration-tests-locally) ## Steps for Implementing a Fix or Feature -There's no strict requirements for adding a fix or feature, just open an issue and a PR +There's no strict requirements for adding a fix or feature, just open an issue and a PR. Code owners will make any necessary amendments ## Conventional Commit @@ -19,14 +17,14 @@ This projects follows the Conventional Commit specification (at least for commit https://www.conventionalcommits.org/en/v1.0.0/ -This allows us to categorize changes and make automated updates to our project version and change log based on the categories being merged. For example, if you commit a feature, you could set the commit message to be: +This allows us to categorize changes and make automated updates to our project version and change log based on the categories being merged + +When merging your PR the you should use squash-merge, and use the default PR commit message with a conventional commit prefix, e.g. ```sh -git commit -m 'feat: my new feature' +feat: add support for AWS SDK v3 (#23) ``` -Once you merge your PR the you should use squash-merge, and the commit message should contain all these [commit messages in the footer](https://github.com/googleapis/release-please#what-if-my-pr-contains-multiple-fixes-or-features), so release-please can track all of the change - Then once the release-please draft PR is merged into the main branch, the version is updated from x.1.x to x.2.x and the change log with be updated with the commit message. The project will also be tagged with the updated version For more information on that this works see [release-please](https://github.com/googleapis/release-please) @@ -49,48 +47,4 @@ ReferenceError: Integration test could not run because USERNAME is undefined or You don't need to run integration tests locally, they'll be triggered in Github when you push your changes to your branch -However, there may be times when you need to fix an integration bug, or make changes to the integration test. In this case you want will want to run integration tests locally. To do this you can ask an code owner for a `.env` file so you can run the tests, or you can setup a Cognito user pool on your own AWS account (or at least have access to one). To create a new user pool follow these steps... - -### _0. Setup an AWS account if you don't have one already_ - -You will need to setup a Cognito user pool, which requires an AWS account. If you don't have an AWS account you can find a guide on how to set one up [here](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html). You will also need to run some AWS CLI commands later on, so [setup AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) if you haven't already - -### _1. Setup your own user pool_ - -You need a Cognito user pool to store user credentials in. To do that follow [this](https://docs.aws.amazon.com/cognito/latest/developerguide/tutorial-create-user-pool.html) guide - -### _2. Create an app in said user pool_ - -Inside this new user pool you need to create an app. When creating the new app make sure you add a client secret and have the ALLOW_USER_SRP_AUTH authentication flow enabled. Follow [this](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html) guide - -### _3. Create a new user_ - -Next you need to [create a new user](https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-create-user-accounts.html). There are no special requirements for the user, as long as you have a username and password that should be enough, but for your own convenience you shouldn't send the confirmation email. You will notice the user has a confirmation status of 'Force change password'. To get around this you need to run this CLI command to permanently set the user's password: - -```sh -aws cognito-idp admin-set-user-password \ - --user-pool-id \ - --username \ - --password \ - --permanent -``` - -### _4. Create a `.env` file with the relevant credentials_ - -Finally in your local repo, create a `.env` file in the root of the project. This file should contain the following: - -```sh -# Credentials used in integration test - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= - -INT_TEST_USERNAME= -INT_TEST_PASSWORD= -INT_TEST_POOL_ID= -INT_TEST_CLIENT_ID= -INT_TEST_SECRET_ID= -INT_TEST_AWS_REGION= -``` - -After all these steps have been completed, you should be able to run integration tests locally. If you have any problems following these steps feel free to open an issue +However, there may be times when you need to fix an integration bug, or make changes to the integration test. In this case you want will want to run integration tests locally. To do this you can ask an code owner for a `.env` file so you can run the tests diff --git a/README.md b/README.md index 8d3414f..d6156f0 100644 --- a/README.md +++ b/README.md @@ -23,31 +23,21 @@ 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: ```ts -import { - createSecretHash, - createPasswordHash, - createSrpSession, - signSrpSession, - wrapAuthChallenge, - wrapInitiateAuth, -} from "cognito-srp-helper"; - // . . . obtain user credentials, IDs, and setup Cognito client const secretHash = createSecretHash(username, clientId, secretId); -const passwordHash = createPasswordHash(username, password, poolId); -const srpSession = createSrpSession(username, passwordHash, poolId); +const srpSession = createSrpSession(username, password, poolId, false); const initiateAuthRes = await cognitoIdentityProviderClient .send( new InitiateAuthCommand( wrapInitiateAuth(srpSession, { - ClientId: CLIENT_ID, + ClientId: clientId, AuthFlow: "USER_SRP_AUTH", AuthParameters: { CHALLENGE_NAME: "SRP_A", SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ), @@ -62,11 +52,11 @@ const respondToAuthChallengeRes = await cognitoIdentityProviderClient .send( new RespondToAuthChallengeCommand( wrapAuthChallenge(signedSrpSession, { - ClientId: CLIENT_ID, + ClientId: clientId, ChallengeName: "PASSWORD_VERIFIER", ChallengeResponses: { SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ), @@ -78,18 +68,9 @@ const respondToAuthChallengeRes = await cognitoIdentityProviderClient // . . . return login tokens from respondToAuthChallengeResponse ``` -Here is an example of how you would use the helper to implement SRP authentication with Cognito using the AWS JavaScript SDK v2 (deprecated): +Here is an example of how you would use the helper to implement SRP authentication with Cognito using the AWS JavaScript SDK v2 (deprecated) using a pre-hashed password: ```ts -import { - createSecretHash, - createPasswordHash, - createSrpSession, - signSrpSession, - wrapAuthChallenge, - wrapInitiateAuth, -} from "cognito-srp-helper"; - // . . . obtain user credentials, IDs, and setup Cognito client const secretHash = createSecretHash(username, clientId, secretId); @@ -104,7 +85,7 @@ const initiateAuthRes = await cognitoIdentityServiceProvider AuthParameters: { CHALLENGE_NAME: "SRP_A", SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ) @@ -122,7 +103,7 @@ const respondToAuthChallengeRes = await cognitoIdentityServiceProvider ChallengeName: "PASSWORD_VERIFIER", ChallengeResponses: { SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ) @@ -134,19 +115,6 @@ const respondToAuthChallengeRes = await cognitoIdentityServiceProvider // . . . return login tokens from respondToAuthChallengeResponse ``` -## 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](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#Protocol), 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 - ## 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. @@ -173,7 +141,7 @@ _string_ - A hash of the secret. This is passed to the SECRET_HASH field Generates the required password hash from the user's credentials and user pool ID -_TIP: If you are authenticating from the backend, you can call this function from the frontend and pass the hash value to the backend. While the user's password is secure being transmitted over HTTPS, this step can add an extra layer of security_ +> 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**: @@ -193,12 +161,16 @@ _string_ - A hash of the user's password. Used to create an SRP session 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 -`passwordHash` - _string_ - A hash of the user's password +`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 @@ -251,6 +223,25 @@ Wraps a _RespondToAuthChallengeRequest_ and attaches the PASSWORD_CLAIM_SECRET_B _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](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol#Protocol), 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 - [amazon-cognito-identity-js](https://www.npmjs.com/package/amazon-cognito-identity-js) - NPM package for the Amplify cognito implementation diff --git a/src/__tests__/integration/sdk-v2.test.ts b/src/__tests__/integration/sdk-v2.test.ts index 64fa06b..58d1b53 100644 --- a/src/__tests__/integration/sdk-v2.test.ts +++ b/src/__tests__/integration/sdk-v2.test.ts @@ -17,24 +17,48 @@ dotenv.config({ path: path.join(process.cwd(), ".env"), }); +// Assert environment variables exist before we begin + const { - INT_TEST_USERNAME: USERNAME = "", - INT_TEST_PASSWORD: PASSWORD = "", - INT_TEST_POOL_ID: POOL_ID = "", - INT_TEST_CLIENT_ID: CLIENT_ID = "", - INT_TEST_SECRET_ID: SECRET_ID = "", - INT_TEST_AWS_REGION: AWS_REGION = "", + AWS_REGION = "", + AWS_ACCESS_KEY_ID = "", + AWS_SECRET_ACCESS_KEY = "", + INT_TEST__USERNAME__USERNAME = "", + INT_TEST__USERNAME__PASSWORD = "", + INT_TEST__USERNAME__POOL_ID = "", + INT_TEST__USERNAME__CLIENT_ID = "", + INT_TEST__USERNAME__SECRET_ID = "", + INT_TEST__EMAIL__USERNAME = "", + INT_TEST__EMAIL__PASSWORD = "", + INT_TEST__EMAIL__POOL_ID = "", + INT_TEST__EMAIL__CLIENT_ID = "", + INT_TEST__EMAIL__SECRET_ID = "", + INT_TEST__PHONE__USERNAME = "", + INT_TEST__PHONE__PASSWORD = "", + INT_TEST__PHONE__POOL_ID = "", + INT_TEST__PHONE__CLIENT_ID = "", + INT_TEST__PHONE__SECRET_ID = "", } = process.env; -// Assert environment variables exist before we begin - Object.entries({ - USERNAME, - PASSWORD, - POOL_ID, - CLIENT_ID, - SECRET_ID, AWS_REGION, + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + INT_TEST__USERNAME__USERNAME, + INT_TEST__USERNAME__PASSWORD, + INT_TEST__USERNAME__POOL_ID, + INT_TEST__USERNAME__CLIENT_ID, + INT_TEST__USERNAME__SECRET_ID, + INT_TEST__EMAIL__USERNAME, + INT_TEST__EMAIL__PASSWORD, + INT_TEST__EMAIL__POOL_ID, + INT_TEST__EMAIL__CLIENT_ID, + INT_TEST__EMAIL__SECRET_ID, + INT_TEST__PHONE__USERNAME, + INT_TEST__PHONE__PASSWORD, + INT_TEST__PHONE__POOL_ID, + INT_TEST__PHONE__CLIENT_ID, + INT_TEST__PHONE__SECRET_ID, }).forEach(([key, value]) => { if (value === "") { throw new ReferenceError(` @@ -48,24 +72,54 @@ Object.entries({ } }); +const positiveCredentials = { + username: { + username: INT_TEST__USERNAME__USERNAME, + password: INT_TEST__USERNAME__PASSWORD, + poolId: INT_TEST__USERNAME__POOL_ID, + clientId: INT_TEST__USERNAME__CLIENT_ID, + secretId: INT_TEST__USERNAME__SECRET_ID, + }, + email: { + username: INT_TEST__EMAIL__USERNAME, + password: INT_TEST__EMAIL__PASSWORD, + poolId: INT_TEST__EMAIL__POOL_ID, + clientId: INT_TEST__EMAIL__CLIENT_ID, + secretId: INT_TEST__EMAIL__SECRET_ID, + }, + phone: { + username: INT_TEST__PHONE__USERNAME, + password: INT_TEST__PHONE__PASSWORD, + poolId: INT_TEST__PHONE__POOL_ID, + clientId: INT_TEST__PHONE__CLIENT_ID, + secretId: INT_TEST__PHONE__SECRET_ID, + }, +}; + describe("SDK v2 integration", () => { - const secretHash = createSecretHash(USERNAME, CLIENT_ID, SECRET_ID); - const passwordHash = createPasswordHash(USERNAME, PASSWORD, POOL_ID); - const srpSession = createSrpSession(USERNAME, passwordHash, POOL_ID); const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider({ region: AWS_REGION, + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY, + }, }); - it("should work with initiateAuth and respondToAuthChallenge", async () => { + it("should work with initiateAuth and respondToAuthChallenge (hashed password)", async () => { + const { username, password, poolId, clientId, secretId } = positiveCredentials.username; + const secretHash = createSecretHash(username, clientId, secretId); + const passwordHash = createPasswordHash(username, password, poolId); + const srpSession = createSrpSession(username, passwordHash, poolId); + const initiateAuthRes = await cognitoIdentityServiceProvider .initiateAuth( wrapInitiateAuth(srpSession, { - ClientId: CLIENT_ID, + ClientId: clientId, AuthFlow: "USER_SRP_AUTH", AuthParameters: { CHALLENGE_NAME: "SRP_A", SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ) @@ -79,11 +133,11 @@ describe("SDK v2 integration", () => { const respondToAuthChallengeRes = await cognitoIdentityServiceProvider .respondToAuthChallenge( wrapAuthChallenge(signedSrpSession, { - ClientId: CLIENT_ID, + ClientId: clientId, ChallengeName: "PASSWORD_VERIFIER", ChallengeResponses: { SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ) @@ -97,17 +151,22 @@ describe("SDK v2 integration", () => { expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); }); - it("should work with adminInitiateAuth and adminRespondToAuthChallenge", async () => { + it("should work with adminInitiateAuth and adminRespondToAuthChallenge (hashed password)", async () => { + const { username, password, poolId, clientId, secretId } = positiveCredentials.username; + const secretHash = createSecretHash(username, clientId, secretId); + const passwordHash = createPasswordHash(username, password, poolId); + const srpSession = createSrpSession(username, passwordHash, poolId); + const adminInitiateAuthRes = await cognitoIdentityServiceProvider .adminInitiateAuth( wrapInitiateAuth(srpSession, { - UserPoolId: POOL_ID, - ClientId: CLIENT_ID, + UserPoolId: poolId, + ClientId: clientId, AuthFlow: "USER_SRP_AUTH", AuthParameters: { CHALLENGE_NAME: "SRP_A", SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ) @@ -121,12 +180,12 @@ describe("SDK v2 integration", () => { const adminRespondToAuthChallengeRes = await cognitoIdentityServiceProvider .adminRespondToAuthChallenge( wrapAuthChallenge(signedSrpSession, { - UserPoolId: POOL_ID, - ClientId: CLIENT_ID, + UserPoolId: poolId, + ClientId: clientId, ChallengeName: "PASSWORD_VERIFIER", ChallengeResponses: { SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ) @@ -139,4 +198,100 @@ describe("SDK v2 integration", () => { expect(adminRespondToAuthChallengeRes.AuthenticationResult).toHaveProperty("AccessToken"); expect(adminRespondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); }); + + it.each(Object.values(positiveCredentials))( + "should work with initiateAuth and respondToAuthChallenge (unhashed password): credentials %#", + async ({ username, password, poolId, clientId, secretId }) => { + const secretHash = createSecretHash(username, clientId, secretId); + const srpSession = createSrpSession(username, password, poolId, false); + + const initiateAuthRes = await cognitoIdentityServiceProvider + .initiateAuth( + wrapInitiateAuth(srpSession, { + ClientId: clientId, + AuthFlow: "USER_SRP_AUTH", + AuthParameters: { + CHALLENGE_NAME: "SRP_A", + SECRET_HASH: secretHash, + USERNAME: username, + }, + }), + ) + .promise() + .catch((err) => { + throw err; + }); + + const signedSrpSession = signSrpSession(srpSession, initiateAuthRes); + + const respondToAuthChallengeRes = await cognitoIdentityServiceProvider + .respondToAuthChallenge( + wrapAuthChallenge(signedSrpSession, { + ClientId: clientId, + ChallengeName: "PASSWORD_VERIFIER", + ChallengeResponses: { + SECRET_HASH: secretHash, + USERNAME: username, + }, + }), + ) + .promise() + .catch((err) => { + throw err; + }); + + expect(respondToAuthChallengeRes).toHaveProperty("AuthenticationResult"); + expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("AccessToken"); + expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); + }, + ); + + it.each(Object.values(positiveCredentials))( + "should work with adminInitiateAuth and adminRespondToAuthChallenge (unhashed password): credentials %#", + async ({ username, password, poolId, clientId, secretId }) => { + const secretHash = createSecretHash(username, clientId, secretId); + const srpSession = createSrpSession(username, password, poolId, false); + + const adminInitiateAuthRes = await cognitoIdentityServiceProvider + .adminInitiateAuth( + wrapInitiateAuth(srpSession, { + UserPoolId: poolId, + ClientId: clientId, + AuthFlow: "USER_SRP_AUTH", + AuthParameters: { + CHALLENGE_NAME: "SRP_A", + SECRET_HASH: secretHash, + USERNAME: username, + }, + }), + ) + .promise() + .catch((err) => { + throw err; + }); + + const signedSrpSession = signSrpSession(srpSession, adminInitiateAuthRes); + + const adminRespondToAuthChallengeRes = await cognitoIdentityServiceProvider + .adminRespondToAuthChallenge( + wrapAuthChallenge(signedSrpSession, { + UserPoolId: poolId, + ClientId: clientId, + ChallengeName: "PASSWORD_VERIFIER", + ChallengeResponses: { + SECRET_HASH: secretHash, + USERNAME: username, + }, + }), + ) + .promise() + .catch((err) => { + throw err; + }); + + expect(adminRespondToAuthChallengeRes).toHaveProperty("AuthenticationResult"); + expect(adminRespondToAuthChallengeRes.AuthenticationResult).toHaveProperty("AccessToken"); + expect(adminRespondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); + }, + ); }); diff --git a/src/__tests__/integration/sdk-v3.test.ts b/src/__tests__/integration/sdk-v3.test.ts index b326fd7..494aa3d 100644 --- a/src/__tests__/integration/sdk-v3.test.ts +++ b/src/__tests__/integration/sdk-v3.test.ts @@ -23,24 +23,44 @@ dotenv.config({ path: path.join(process.cwd(), ".env"), }); +// Assert environment variables exist before we begin + const { - INT_TEST_USERNAME: USERNAME = "", - INT_TEST_PASSWORD: PASSWORD = "", - INT_TEST_POOL_ID: POOL_ID = "", - INT_TEST_CLIENT_ID: CLIENT_ID = "", - INT_TEST_SECRET_ID: SECRET_ID = "", - INT_TEST_AWS_REGION: AWS_REGION = "", + AWS_REGION = "", + INT_TEST__USERNAME__USERNAME = "", + INT_TEST__USERNAME__PASSWORD = "", + INT_TEST__USERNAME__POOL_ID = "", + INT_TEST__USERNAME__CLIENT_ID = "", + INT_TEST__USERNAME__SECRET_ID = "", + INT_TEST__EMAIL__USERNAME = "", + INT_TEST__EMAIL__PASSWORD = "", + INT_TEST__EMAIL__POOL_ID = "", + INT_TEST__EMAIL__CLIENT_ID = "", + INT_TEST__EMAIL__SECRET_ID = "", + INT_TEST__PHONE__USERNAME = "", + INT_TEST__PHONE__PASSWORD = "", + INT_TEST__PHONE__POOL_ID = "", + INT_TEST__PHONE__CLIENT_ID = "", + INT_TEST__PHONE__SECRET_ID = "", } = process.env; -// Assert environment variables exist before we begin - Object.entries({ - USERNAME, - PASSWORD, - POOL_ID, - CLIENT_ID, - SECRET_ID, AWS_REGION, + INT_TEST__USERNAME__USERNAME, + INT_TEST__USERNAME__PASSWORD, + INT_TEST__USERNAME__POOL_ID, + INT_TEST__USERNAME__CLIENT_ID, + INT_TEST__USERNAME__SECRET_ID, + INT_TEST__EMAIL__USERNAME, + INT_TEST__EMAIL__PASSWORD, + INT_TEST__EMAIL__POOL_ID, + INT_TEST__EMAIL__CLIENT_ID, + INT_TEST__EMAIL__SECRET_ID, + INT_TEST__PHONE__USERNAME, + INT_TEST__PHONE__PASSWORD, + INT_TEST__PHONE__POOL_ID, + INT_TEST__PHONE__CLIENT_ID, + INT_TEST__PHONE__SECRET_ID, }).forEach(([key, value]) => { if (value === "") { throw new ReferenceError(` @@ -54,25 +74,51 @@ Object.entries({ } }); +const positiveCredentials = { + username: { + username: INT_TEST__USERNAME__USERNAME, + password: INT_TEST__USERNAME__PASSWORD, + poolId: INT_TEST__USERNAME__POOL_ID, + clientId: INT_TEST__USERNAME__CLIENT_ID, + secretId: INT_TEST__USERNAME__SECRET_ID, + }, + email: { + username: INT_TEST__EMAIL__USERNAME, + password: INT_TEST__EMAIL__PASSWORD, + poolId: INT_TEST__EMAIL__POOL_ID, + clientId: INT_TEST__EMAIL__CLIENT_ID, + secretId: INT_TEST__EMAIL__SECRET_ID, + }, + phone: { + username: INT_TEST__PHONE__USERNAME, + password: INT_TEST__PHONE__PASSWORD, + poolId: INT_TEST__PHONE__POOL_ID, + clientId: INT_TEST__PHONE__CLIENT_ID, + secretId: INT_TEST__PHONE__SECRET_ID, + }, +}; + describe("SDK v3 integration", () => { - const secretHash = createSecretHash(USERNAME, CLIENT_ID, SECRET_ID); - const passwordHash = createPasswordHash(USERNAME, PASSWORD, POOL_ID); - const srpSession = createSrpSession(USERNAME, passwordHash, POOL_ID); const cognitoIdentityProviderClient = new CognitoIdentityProviderClient({ region: AWS_REGION, }); - it("should work with InitiateAuthCommand and RespondToAuthChallengeCommand", async () => { + it("should work with InitiateAuthCommand and RespondToAuthChallengeCommand (hashed password)", async () => { + const { username, password, poolId, clientId, secretId } = positiveCredentials.username; + const secretHash = createSecretHash(username, clientId, secretId); + const passwordHash = createPasswordHash(username, password, poolId); + const srpSession = createSrpSession(username, passwordHash, poolId); + const initiateAuthRes = await cognitoIdentityProviderClient .send( new InitiateAuthCommand( wrapInitiateAuth(srpSession, { - ClientId: CLIENT_ID, + ClientId: clientId, AuthFlow: "USER_SRP_AUTH", AuthParameters: { CHALLENGE_NAME: "SRP_A", SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ), @@ -87,11 +133,11 @@ describe("SDK v3 integration", () => { .send( new RespondToAuthChallengeCommand( wrapAuthChallenge(signedSrpSession, { - ClientId: CLIENT_ID, + ClientId: clientId, ChallengeName: "PASSWORD_VERIFIER", ChallengeResponses: { SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ), @@ -105,18 +151,23 @@ describe("SDK v3 integration", () => { expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); }); - it("should work with AdminInitiateAuthCommand and AdminRespondToAuthChallengeCommand", async () => { + it("should work with AdminInitiateAuthCommand and AdminRespondToAuthChallengeCommand (hashed password)", async () => { + const { username, password, poolId, clientId, secretId } = positiveCredentials.username; + const secretHash = createSecretHash(username, clientId, secretId); + const passwordHash = createPasswordHash(username, password, poolId); + const srpSession = createSrpSession(username, passwordHash, poolId); + const initiateAuthRes = await cognitoIdentityProviderClient .send( new AdminInitiateAuthCommand( wrapInitiateAuth(srpSession, { - UserPoolId: POOL_ID, - ClientId: CLIENT_ID, + UserPoolId: poolId, + ClientId: clientId, AuthFlow: "USER_SRP_AUTH", AuthParameters: { CHALLENGE_NAME: "SRP_A", SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ), @@ -131,12 +182,12 @@ describe("SDK v3 integration", () => { .send( new AdminRespondToAuthChallengeCommand( wrapAuthChallenge(signedSrpSession, { - UserPoolId: POOL_ID, - ClientId: CLIENT_ID, + UserPoolId: poolId, + ClientId: clientId, ChallengeName: "PASSWORD_VERIFIER", ChallengeResponses: { SECRET_HASH: secretHash, - USERNAME, + USERNAME: username, }, }), ), @@ -149,4 +200,104 @@ describe("SDK v3 integration", () => { expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("AccessToken"); expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); }); + + it.each(Object.values(positiveCredentials))( + "should work with InitiateAuthCommand and RespondToAuthChallengeCommand (unhashed password): credentials %#", + async ({ username, password, poolId, clientId, secretId }) => { + 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, + }, + }), + ), + ) + .catch((err) => { + throw err; + }); + + 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, + }, + }), + ), + ) + .catch((err) => { + throw err; + }); + + expect(respondToAuthChallengeRes).toHaveProperty("AuthenticationResult"); + expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("AccessToken"); + expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); + }, + ); + + it.each(Object.values(positiveCredentials))( + "should work with AdminInitiateAuthCommand and AdminRespondToAuthChallengeCommand (unhashed password): credentials %#", + async ({ username, password, poolId, clientId, secretId }) => { + const secretHash = createSecretHash(username, clientId, secretId); + const srpSession = createSrpSession(username, password, poolId, false); + + const initiateAuthRes = await cognitoIdentityProviderClient + .send( + new AdminInitiateAuthCommand( + wrapInitiateAuth(srpSession, { + UserPoolId: poolId, + ClientId: clientId, + AuthFlow: "USER_SRP_AUTH", + AuthParameters: { + CHALLENGE_NAME: "SRP_A", + SECRET_HASH: secretHash, + USERNAME: username, + }, + }), + ), + ) + .catch((err) => { + throw err; + }); + + const signedSrpSession = signSrpSession(srpSession, initiateAuthRes); + + const respondToAuthChallengeRes = await cognitoIdentityProviderClient + .send( + new AdminRespondToAuthChallengeCommand( + wrapAuthChallenge(signedSrpSession, { + UserPoolId: poolId, + ClientId: clientId, + ChallengeName: "PASSWORD_VERIFIER", + ChallengeResponses: { + SECRET_HASH: secretHash, + USERNAME: username, + }, + }), + ), + ) + .catch((err) => { + throw err; + }); + + expect(respondToAuthChallengeRes).toHaveProperty("AuthenticationResult"); + expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("AccessToken"); + expect(respondToAuthChallengeRes.AuthenticationResult).toHaveProperty("RefreshToken"); + }, + ); }); diff --git a/src/__tests__/mocks/data.ts b/src/__tests__/mocks/data.ts index 8603724..c3da960 100644 --- a/src/__tests__/mocks/data.ts +++ b/src/__tests__/mocks/data.ts @@ -1,27 +1,33 @@ import { AuthFlowType, ChallengeNameType } from "@aws-sdk/client-cognito-identity-provider"; -const username = "username"; -const password = "password"; -const poolId = "eu-west-2_bBpjKlyj6"; -const clientId = "EPswPGbpdBLZnh9XW134hK7qYe"; -const secretId = "ps50nb7hd1umdnmlt1xa9nwiscqvdvzy5ijw63vcacd09yihc2b"; -const secretHash = "oUkNFqfm7UjLds8vXFWarT1l3gqORGoztf+EyFmoX2Q="; -const passwordHash = "266629fd96baf8f433a4f01562eea59dd99a721dac75ae600e06bc697b2016ff"; -const poolIdAbbr = "bBpjKlyj6"; -const timestamp = "Tue Feb 1 03:04:05 UTC 2000"; -const smallA = "abcdef0123456789"; -const largeA = +export const sub = "a2c2e290-6d0f-4a08-a5ca-f0162935f3a6"; +export const username = "john.doe"; +export const email = "yorej57765@comsb.com"; +export const phone = "01234567890"; +export const password = "Qwerty1!"; +export const passwordHash = "cd9896b264dc8dca270b0b74b039cf775ea70afb06bd253f5e0ffe7197224aa3"; +export const poolId = "eu-west-2_eYpv1mFHB"; +export const clientId = "18u8119jgbpr464n28s1itk2mq"; +export const secretId = "ps50nb7hd1umdnmlt1xa9nwiscqvdvzy5ijw63vcacd09yihc2b"; +export const secretHash = "QW3YkSgjkc9VNMtbTkuQflK54A6+9GS8ZiRDj0mSvPI="; +export const poolIdAbbr = "eYpv1mFHB"; +export const timestamp = "Tue Feb 1 03:04:05 UTC 2000"; +export const smallA = "abcdef0123456789"; +export const largeA = "362be8c4c29e414549c30c6679ddbb556c27717d80c8feec68257bf5edcd855fbd23590841c1f792dd24a09243c00d4fbb8cc8047fbe1c95da7b467d29269d2db462c6268f10ef5a3c23f71865a54bae6e48523e6a7a9b65d115c66a2f5b4ec5b7384c9796dc6b2d422ea630e1821104ab05cb4a4f292f991d771be4fdba39bc6ff8dce0f0addaaaf172d908d08544d573a2222f1e198caf65f5e6445848ed23b5bbfff0b5bc00ca54b2831a200577220cfe4f5de20b66baffbfc8d3a16af6f6a7a590e0764ccdf918daf922df1ed0a695702c9bd63a69b09b4c2fb8a3bffee5f89744639607e1920ee006d81f09cba7682e75be5b407fb5cb1f706c0dbf7a9325b5c56ea53333ba1aecab647beae4b2b44c0912814609560fda8d86e6aff0b2317e339d2d3b422069d8e68fca8a39c43acb360d4285bb2dd076bb58b41e07be255f349537adaae3bf5260b2c7e066fb77904ebcee5005cc367f926482f9b405952f50e1182049ac49847a048fca4fbe13c6538eea38132c9bc43c9b05690bb7"; -const largeB = - "a2a9be7148a622a87b92513959336c923ab748fdde47e1eae8e0b4495bfc950e65a4c2cf09ac9ab38c85dbc31100bda601bb44d03d8c8a47770cdace442bea4c30ac83df807c2d2dfd6d8b449178c4c558108d141bdd79f8695d40ce25d77b1d7715a53b0022782b71105710e476b484e7c01d9625eafe4d50aff1808712d4c2636a7bb83b008fb8f398d79c5dfe59074ee0df3240253e5719c18b343f49ef5d975d89e2c481628eadca30a90ab6c41d7b9308aa6f6fc618813393ace0f52562f51f26c3fc3160e9dc99f78e9bc22758eeb7f22c137a962de99a66feb7a23e179a599813c8431c364ee193b64eef9bca04117ceba50140bafe1a0aea262c0b9bcde235866da2f1c174063d685ad67f231fcef7dca62a327f6cf2279b5fa17383acd1868629d75523e87e14cdb214ea14ce6d47228e350e4d1d451a1238886d6b3dae1d59b79321ab740c995b28c6e03a61cf76383ce1906f86389927d3575e751cfe4ab50c1eef52dd79aee69512b88f237bd74d04e8cc72d0ab94c0ad934f26"; -const salt = "8f6a1dad94d7b82c5e3031d21a251b0f"; -const secret = - "F+iU1RhIMWd3K64Qg+J/eud+WYMsW1tDULDQ4boHjzgdNFiBSKFX3BvoMHZJ2MjKv6nKMRgjifbx5sjdrBi2n+Nat+9E1QD0rzeJ2l2oOeVXruiwItacGO5AjM9PU1sFtZu4N13oNbsx9DUY7diIB+ksKz8LjxJV4FP055O1OJzEGbSY+RHAw3d3HsY2xsE9Zy6bD5FpSrONwKZWWUpZXSexPiDHzGIcRyBBYXwOKAJ+1pCtZW/Jg+MuW6hiYfTMEGk8vr/35TmiAHjYRe1urxElaa+fJi41j1tpX5FhrHfNp3WmuVj6cWJwkhZ/GKGL000lwsXDEY83C2v2czpjP1VwyDf2UPxI1BNPMVjCWZx5V/9D5SniPUWCNpwyh+ZSCVvRJxQitMcV0klswY36itXXn7OCurUjgqViUIBPbrVDcbuZ49tR4UoHlQXNzsmzXwlGwyHvNUHjNAT/6/qAAtmHwJbT6GZzceN1g+33P0Eq2BIWxj16pnhPKykrGuvIV3TpcL9FY0CcTKqozRfC2pOWenT08md++uq39z9OP+wA/7Fjt1ceLOiDcRhEW0xhoBqu/3V7RDNDUqV2bLH7rEq7Q5PCGhsRu8h9crUbVtNOUrna/o7HmRy0XiBQdjLP7Xe6uASNZAAso4eUZce0oc2LIpHvAx6BK8cw8rp2Pcg+ZnC0HgcpBEF6sHJEcH4GmZNJ10c/exITniVKSJkP1C8iPqulNug6OMmSav3WpMvj/qb/t8JPNqbePj99fANkteG7JPUY9LoE/wsBDIeYEA4lZ4HvKZw9g8RbltYNmOQAa6K4XXI0AEspx1+TT5A+cJl48BPk+f46SCThCzK/8N9XHaerojUqu4EWW1l5c+WRaOCdvo/HQidTn1VQJ6lcyJwcuyoxy7MlESMfs1G9rgehHbYjWNxoUwX/AwFfNzS/7dMENhDFx+ZUsT0wgrn+iMhfJJPHG4lt0N/R6JjQ8IOgaUZ7CZdrL7pQxA5v0eqWwZ/cYJVAI+mGLez1ldYQRvZkw0dOkpCjCDrw7ZTKnsyRLDE6KyyNT+ykJq7aGyAHb5nxualN0B8OnqY1hh+tZ5xTOh9jlnvBo7dwfsbZpjsGGLiMLbc411cT+FUONrlPdK9wDEZdQ8jcys4PfeCzcju/VnNEbbv1EuxG5bIWGApSOVf04p2/0bBWn8VJkKoCsC9+QhV2KwTRfCLZWloYIc/N1QuzXf328m9FG3vihrXvkgcxxsSbUjMb4QhR7DkYIN6kWsguBOVTybdTi4vlNqFzX8bJtK3uQNKC98yYzZjR40HFOydBJjLckUandwu1blgqM3IwlmYWMiPcFx2SGQRIufq8hLOhyE12ulT9KI4c2D+yLn7hASICW/3z7LgfuWq6j1FDVlmSqM00C8wZ84DzT056HLETpWpl1Gh3QRJPkReqRKlY0Bqrk5koMfmRuGht/WnyXxs0gSAkWFOevpvYopyHBhMTdc7DlGE70QcRJiBeuJkKNOoih+lOkHGFof5xnFWzaZkuouBZFntSn4xZxEAb1Tl56k1b8oCIHLvrsnlqxMHfNYlnQUO+gnhFgDc6AlyQCtc6+wlulggchkPFMgjlaN/w6h3N9697ksRnySNMNAkCx+VgP+qbgn24pGYWWtlCWWpsdkxU6OzpFQwBchqBw9LUB5MyqjwWLssMxlJxg0KgXnRNfwv5jd0sAE5KvT4vFCt0FA=="; -const passwordSignature = "AmaS40dQC4mBIgVaKNkAvWpYBmHUi/gv/XKVVCr8xyE="; +export const largeB = + "bc071b539bfaa44b26a4f4a917dcc4b90291f3fea737baa555c0dfc792f11849902859dd52976fd7367130d91969670e8a0beb66d044641709d7a9ea609e2cc5baac6837e1ee8f8d9ccdce535be1dee59d4c893d9c9a1f06ddacb9d927e29c0e8bb1b8dda315f297e82c6095570ae3ab28a9110dfc4367296d77898b1d69d029f76e16ffebed59fc568548a9fb54a1a462440b19b4e4d89ae414374654788d599d0635f9b8a7a5c6a8556e675c4e01690d5324eecd092d269b2be31fa2dc7192cb0bec8a390ca4ce791c8e8cc9ed7258929430a2802a2b9dadd985a004d584e883621ff3518e223a6dbab5071bc844e4ee7d12f6c29bd9197f0fefcee91020b0903596cf170efd27648eb5de5ab2961032cf759dbc48dadb6db0dd585bb9c1e9f73bbea67a7ca406e26d4333c63d5339fa8c0f51fc12df554737da39fc780c0a648e884600f1bf9398cedddaee7d6db6ace4e0007d97f7322d7270dc25c6e6ae000e54b62f8cb868b16e16eadb4c8c87f3baae1c1b8e59aabb73771e62c0b898"; +export const salt = "baf4431cdaa37c04c0d655a99f5e9b9b"; +export const secret = + "gn7F/ENIPPk/KZ7P2xYr4S0pZhCn29Y0nxXBtEPEc8Tt2cqbQpWorIuwSAC0g3fpDW9djD5+b9v30TctMTiVt6xjGaZeuWzzgAnVrc0l0xROzJItEQM1V96ySkrfFbL4tKuZokrqiszsRkgwnu4SGBz3akVpUD8pZnAg1Ycfc3LLSyQETFTndRHjS8UFGIovw7wqQfxKNKDC1nY2XYWR1nFuEIvHXb+Tvc76IxNZcaEvl5sAuTjwpVlmfwPSOELXnYxvDxUcFYO3sNguNqahn6aH4j16ccEZ/Wqoubn7QkomXV3Qdo5khQbw4F9CWFaxCHoRZwFAc8RQ4oGiIHLvuMb/Y/JhGTL3HefSkBXQ+uvoPK7YdgY8yn3SejYv4xD8lw+KLTPy1EbCXX9Aq1uwgjzeSygLl7/tkf1C/YzhefXEOfaLk2eNRlAschYunQ7WeIZWBYzW+38+r4bQpaecj8GcH/ycAOpacfoLB+/q3E/dHsFOFlzetYO6Qll5AXEv4hzJZgNh1/2zK3N8j2JBUjZs2h60Rh8OS+ZcQGa7ICPfS7XDvSV8U6Xan3j2m0Azz2ara+1I4MYDVj5P4/BneVDh4L+6Dl3EWEAF9UO8Ur1/5seiOVcAQe3TlwN9UWgmOZHIq/UGU14ofpmZI46WWS3qB+yBPHbWWJ0MMFFtRwCTzFl+2kRGthRR5A+6MSuLCfdcQwqb6FYRZEdgXZzMa6Td0Pog/5TFqxygkRB0P3Yq4lfOnRKI6cC3+2g8uMsEmOJfzsVrsfi6GTAPrP+lIPpNe12ZquFzPGi9Z68Odj+aNDYChalBpuvJgxWR2EFuyj2MebKz6Zo/5V/w964aTTYjUt1pPHURLx3MKl7Ns1xF1lklQ1DAOQYNrVWxWJRs4TxipduEq4N02uh9XKWzXxLJuHsc1B9e1Dc+1sHs122POq7i/VYoln6LhO30o32lDHOGa56krde0N/+FY7034zB1pqL8VuDd2/8o8XZ+D/ez/1gBwQ2y1q6CRvvZmNyYBIC8an/Vgb++e5zgx6Cb43o9+PEFc8UEA8QljTgZ2m+kwjia1TSYeyYnKQOIqCKMkZa6IBIwMZ6AHIDSWk//IR/UMiq4QGfb5CJFPYOTKSGXEU9dFPYbx+pZmq/Fq7QgFCM/I7M6jKZPwn+T4/RGLEg1YNRwQS/oQI/uu5iF9sIbtWO5XTRQ8q2ld4s3tEFflYdmELHXrt1UuFaCi82iTJZAgFPeIx9mHPb0U+I7iutfEPogIE5b/2T+qJhIBgeIQUVsJuo+uLdriFm1c7YwBMBTJWC9R2mL+2x6quavpx7u0zgHrmbDxFx6+nBl0DEZxsY5h8AEDUVhyi2w+7aK9T/uh7Pz3/fH+AwXfWcA1cBueYYHX0Due6+3hRAHfOokXTbi27+YoS2AKuXDl2EibHedm5lb1Tg18V66dXtGsvpIn/OUWO2FphUGdtCMgZc51ahYTiN1m6Qqx+z1DFRM2BLw6HLxb1uT1OmDyucvCXIsMl4vNyyeIdJwstIbQrECxg1Fdow6jHrzKyJw/FKsYGSnyDRk++1jTt3J46wz16S45XaMSDG8RMH7AKKeAX/lfJ0d247ajB4U8Hei1h1kclX/DAYp7A0KyoCG7Qt9hZGsxhsloT2eYohHEKshPDs2WU48WtuHwgXydkTABvx3b1ixLp2CEuQiMyy3jZpvDD8WGaqWqbQgnH/yQw=="; +export const passwordSignature = "RrSiIBQazZkaxHQf34oRro1qjzfEvwdP6/Avltpd34E="; // This object isn't typed because it is a collection of external inputs export const mockCredentials = { + sub, username, + email, + phone, password, poolId, clientId, @@ -33,7 +39,9 @@ export const mockCredentials = { export const mockSession = { username, - passwordHash, + password: passwordHash, + isHashed: true, + poolId, poolIdAbbr, timestamp, smallA, @@ -72,6 +80,7 @@ export const mockInitiateAuthResponse = { SRP_B: largeB, SALT: salt, SECRET_BLOCK: secret, + USER_ID_FOR_SRP: sub, }, }; diff --git a/src/__tests__/mocks/index.ts b/src/__tests__/mocks/index.ts index c159d9f..402e901 100644 --- a/src/__tests__/mocks/index.ts +++ b/src/__tests__/mocks/index.ts @@ -1 +1,2 @@ +export * as data from "./data"; export * as factories from "./factories"; diff --git a/src/__tests__/test-cases/credentials.ts b/src/__tests__/test-cases/credentials.ts index 1bf1d51..8b70b1e 100644 --- a/src/__tests__/test-cases/credentials.ts +++ b/src/__tests__/test-cases/credentials.ts @@ -18,6 +18,9 @@ export const positiveCredentials: Record = { allowSpecialCharacters: true, }), }), + usernamePhone: mockCredentialsFactory({ + username: faker.phone.number(), + }), usernameUuid: mockCredentialsFactory({ username: faker.datatype.uuid(), }), diff --git a/src/__tests__/test-cases/initiate-auth-response.ts b/src/__tests__/test-cases/initiate-auth-response.ts index 6c5f211..0902e04 100644 --- a/src/__tests__/test-cases/initiate-auth-response.ts +++ b/src/__tests__/test-cases/initiate-auth-response.ts @@ -19,7 +19,8 @@ export const positiveInitiateAuthResponses: Record largeBShort: mockInitiateAuthResponseFactory({ ChallengeParameters: { ...ChallengeParameters, - SRP_B: faker.random.alphaNumeric(1, { casing: "lower" }), + // 1 / 62 chance to return "0" which will trigger a AbortOnZeroBSrpError, so ban the char + SRP_B: faker.random.alphaNumeric(1, { casing: "lower", bannedChars: "0" }), }, }), largeBLong: mockInitiateAuthResponseFactory({ diff --git a/src/__tests__/test-cases/srp-sessions-signed.ts b/src/__tests__/test-cases/srp-sessions-signed.ts index 7af71be..feeaf55 100644 --- a/src/__tests__/test-cases/srp-sessions-signed.ts +++ b/src/__tests__/test-cases/srp-sessions-signed.ts @@ -18,6 +18,9 @@ export const positiveSrpSessionsSigned: Record = { allowSpecialCharacters: true, }), }), + usernamePhone: mockSrpSessionSignedFactory({ + username: faker.phone.number(), + }), usernameUuid: mockSrpSessionSignedFactory({ username: faker.datatype.uuid(), }), @@ -27,12 +30,15 @@ export const positiveSrpSessionsSigned: Record = { usernameEmpty: mockSrpSessionSignedFactory({ username: "", }), - // passwordHash + // password + passwordPlain: mockSrpSessionSignedFactory({ + password: faker.internet.password(), + }), passwordHashRandom: mockSrpSessionSignedFactory({ - passwordHash: faker.random.alphaNumeric(64, { casing: "lower" }), + password: faker.random.alphaNumeric(64, { casing: "lower" }), }), passwordHashEmpty: mockSrpSessionSignedFactory({ - passwordHash: "", + password: "", }), // poolIdAbbr poolIdAbbrRandom: mockSrpSessionSignedFactory({ @@ -84,7 +90,8 @@ export const positiveSrpSessionsSigned: Record = { largeA: faker.random.alphaNumeric(1024, { casing: "lower" }), }), largeAShort: mockSrpSessionSignedFactory({ - largeA: faker.random.alphaNumeric(1, { casing: "lower" }), + // 1 / 62 chance to return "0" which will trigger a AbortOnZeroASrpError, so ban the char + largeA: faker.random.alphaNumeric(1, { casing: "lower", bannedChars: "0" }), }), largeALong: mockSrpSessionSignedFactory({ largeA: faker.random.alphaNumeric(10000, { casing: "lower" }), @@ -94,7 +101,8 @@ export const positiveSrpSessionsSigned: Record = { largeB: faker.random.alphaNumeric(1024, { casing: "lower" }), }), largeBShort: mockSrpSessionSignedFactory({ - largeB: faker.random.alphaNumeric(1, { casing: "lower" }), + // 1 / 62 chance to return "0" which will trigger a AbortOnZeroBSrpError, so ban the char + largeB: faker.random.alphaNumeric(1, { casing: "lower", bannedChars: "0" }), }), largeBLarge: mockSrpSessionSignedFactory({ largeB: faker.random.alphaNumeric(10000, { casing: "lower" }), diff --git a/src/__tests__/test-cases/srp-sessions.ts b/src/__tests__/test-cases/srp-sessions.ts index 17df4e9..6fdea87 100644 --- a/src/__tests__/test-cases/srp-sessions.ts +++ b/src/__tests__/test-cases/srp-sessions.ts @@ -17,6 +17,9 @@ export const positiveSrpSessions: Record = { allowSpecialCharacters: true, }), }), + usernamePhone: mockSrpSessionFactory({ + username: faker.phone.number(), + }), usernameUuid: mockSrpSessionFactory({ username: faker.datatype.uuid(), }), @@ -26,12 +29,15 @@ export const positiveSrpSessions: Record = { usernameEmpty: mockSrpSessionFactory({ username: "", }), - // passwordHash + // password + passwordPlain: mockSrpSessionFactory({ + password: faker.internet.password(), + }), passwordHashRandom: mockSrpSessionFactory({ - passwordHash: faker.random.alphaNumeric(64, { casing: "lower" }), + password: faker.random.alphaNumeric(64, { casing: "lower" }), }), passwordHashEmpty: mockSrpSessionFactory({ - passwordHash: "", + password: "", }), // poolIdAbbr poolIdAbbrRandom: mockSrpSessionFactory({ @@ -83,7 +89,8 @@ export const positiveSrpSessions: Record = { largeA: faker.random.alphaNumeric(1024, { casing: "lower" }), }), largeAShort: mockSrpSessionFactory({ - largeA: faker.random.alphaNumeric(1, { casing: "lower" }), + // 1 / 62 chance to return "0" which will trigger a AbortOnZeroASrpError, so ban the char + largeA: faker.random.alphaNumeric(1, { casing: "lower", bannedChars: "0" }), }), largeALong: mockSrpSessionFactory({ largeA: faker.random.alphaNumeric(10000, { casing: "lower" }), diff --git a/src/__tests__/unit/create-password-hash.test.ts b/src/__tests__/unit/create-password-hash.test.ts index 785c214..e0c4ca6 100644 --- a/src/__tests__/unit/create-password-hash.test.ts +++ b/src/__tests__/unit/create-password-hash.test.ts @@ -5,18 +5,17 @@ import { positiveCredentials } from "../test-cases"; describe("createPasswordHash", () => { describe("positive", () => { it("should create the correct password hash", () => { - const credentials = mockCredentialsFactory(); - const { username, password, poolId, passwordHash: expected } = credentials; - const passwordHash = createPasswordHash(username, password, poolId); - expect(passwordHash).toEqual(expected); + const { sub, password, poolId, passwordHash: expected } = mockCredentialsFactory(); + const hash = createPasswordHash(sub, password, poolId); + expect(hash).toEqual(expected); }); it.each(Object.values(positiveCredentials))( "should create a password hash with the correct format: credentials %#", (credentials) => { - const { username, password, poolId } = credentials; - const passwordHash = createPasswordHash(username, password, poolId); - expect(passwordHash).toMatch(/^[a-z0-9]{64}$/); + const { sub, password, poolId } = credentials; + const hash = createPasswordHash(sub, password, poolId); + expect(hash).toMatch(/^[a-z0-9]{64}$/); }, ); }); diff --git a/src/__tests__/unit/create-secret-hash.test.ts b/src/__tests__/unit/create-secret-hash.test.ts index 4544d70..81ae6f1 100644 --- a/src/__tests__/unit/create-secret-hash.test.ts +++ b/src/__tests__/unit/create-secret-hash.test.ts @@ -5,18 +5,17 @@ import { positiveCredentials } from "../test-cases"; describe("createSecretHash", () => { describe("positive", () => { it("should create the correct secret hash", () => { - const credentials = mockCredentialsFactory(); - const { username, clientId, secretId, secretHash: expected } = credentials; - const secretHash = createSecretHash(username, clientId, secretId); - expect(secretHash).toEqual(expected); + const { sub, clientId, secretId, secretHash: expected } = mockCredentialsFactory(); + const hash = createSecretHash(sub, clientId, secretId); + expect(hash).toEqual(expected); }); it.each(Object.values(positiveCredentials))( "should create a secret hash with the correct format: credentials %#", (credentials) => { - const { username, clientId, secretId } = credentials; - const secretHash = createSecretHash(username, clientId, secretId); - expect(secretHash).toMatch(/^[a-zA-Z0-9+=/]+$/); + const { sub, clientId, secretId } = credentials; + const hash = createSecretHash(sub, clientId, secretId); + expect(hash).toMatch(/^[a-zA-Z0-9+=/]+$/); }, ); }); diff --git a/src/__tests__/unit/create-srp-session.test.ts b/src/__tests__/unit/create-srp-session.test.ts index 0d2624d..3dc2642 100644 --- a/src/__tests__/unit/create-srp-session.test.ts +++ b/src/__tests__/unit/create-srp-session.test.ts @@ -9,15 +9,14 @@ import { positiveCredentials, positiveTimestamps } from "../test-cases"; describe("createSrpSession", () => { describe("positive", () => { - it("should create the correct SRP session", () => { + it("should create the correct SRP session for a hashed password", () => { // ensure randomBytes returns what we expect const { smallA } = mockSrpSessionFactory(); jest.spyOn(utils, "randomBytes").mockReturnValueOnce(Buffer.from(smallA, "hex")); // Tue Feb 1 03:04:05 UTC 2000 in Unix timestamp jest.useFakeTimers().setSystemTime(new Date(949374245000)); - const credentials = mockCredentialsFactory(); - const { username, passwordHash, poolId } = credentials; + const { username, passwordHash, poolId } = mockCredentialsFactory(); const session = createSrpSession(username, passwordHash, poolId); const expected = mockSrpSessionFactory(); expect(session).toEqual(expected); @@ -25,6 +24,21 @@ describe("createSrpSession", () => { jest.useRealTimers(); }); + it("should create the correct SRP session for a unhashed password", () => { + // ensure randomBytes returns what we expect + const { smallA } = mockSrpSessionFactory(); + jest.spyOn(utils, "randomBytes").mockReturnValueOnce(Buffer.from(smallA, "hex")); + // Tue Feb 1 03:04:05 UTC 2000 in Unix timestamp + jest.useFakeTimers().setSystemTime(new Date(949374245000)); + + const { username, password, poolId } = mockCredentialsFactory({ password: "Qwerty1!" }); + const session = createSrpSession(username, password, poolId, false); + const expected = mockSrpSessionFactory({ password, isHashed: false }); + expect(session).toEqual(expected); + + jest.useRealTimers(); + }); + it.each(Object.values(positiveCredentials))( "should create a SRP session with the correct format: credentials %#", (credentials) => { @@ -32,7 +46,7 @@ describe("createSrpSession", () => { const session = createSrpSession(username, passwordHash, poolId); expect(session.username).toMatch(username); - expect(session.passwordHash).toMatch(passwordHash); + expect(session.password).toMatch(passwordHash); expect(session.poolIdAbbr).toMatch(poolId.split("_")[1]); expect(session.timestamp).toMatch( /(Sun|Mon|Tue|Wed|Thu|Fri|Sat){1} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec){1} [1-3]?[0-9] (2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9]) UTC [0-9]{1,4}/, @@ -46,8 +60,7 @@ describe("createSrpSession", () => { "should create a timestamp with the correct format: epoch %#", (epoch) => { jest.useFakeTimers().setSystemTime(new Date(epoch)); - const credentials = mockCredentialsFactory(); - const { username, passwordHash, poolId } = credentials; + const { username, passwordHash, poolId } = mockCredentialsFactory(); const { timestamp } = createSrpSession(username, passwordHash, poolId); expect(timestamp).toMatch( /(Sun|Mon|Tue|Wed|Thu|Fri|Sat){1} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec){1} [1-3]?[0-9] (2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9]) UTC [0-9]{1,4}/, @@ -57,8 +70,7 @@ describe("createSrpSession", () => { ); it("should not create the same SRP session on successive calls", () => { - const credentials = mockCredentialsFactory(); - const { username, passwordHash, poolId } = credentials; + const { username, passwordHash, poolId } = mockCredentialsFactory(); const sessionA = createSrpSession(username, passwordHash, poolId); const sessionB = createSrpSession(username, passwordHash, poolId); expect(sessionA).not.toEqual(sessionB); @@ -67,8 +79,7 @@ describe("createSrpSession", () => { describe("negative", () => { it("should throw a AbortOnZeroASrpError if SRP A is 0", () => { - const credentials = mockCredentialsFactory(); - const { username, passwordHash, poolId } = credentials; + const { username, passwordHash, poolId } = mockCredentialsFactory(); // make sure our A = G % a ^ N calculation returns 0 diff --git a/src/__tests__/unit/sign-srp-session.test.ts b/src/__tests__/unit/sign-srp-session.test.ts index ce30911..fd30d5d 100644 --- a/src/__tests__/unit/sign-srp-session.test.ts +++ b/src/__tests__/unit/sign-srp-session.test.ts @@ -18,14 +18,14 @@ import { import { negativeInitiateAuthResponses as negativeResponses, positiveInitiateAuthResponses as positiveResponses, - positiveSrpSessions as positiveSessions, + positiveSrpSessionsSigned as positiveSessions, } from "../test-cases"; const { ChallengeParameters } = mockInitiateAuthResponseFactory(); describe("signSrpSession", () => { describe("positive", () => { - it("should create the correct signed SRP session", () => { + it("should create the correct signed SRP session for a hashed password", () => { const session = mockSrpSessionFactory(); const response = mockInitiateAuthResponseFactory(); const sessionSigned = signSrpSession(session, response); @@ -33,6 +33,14 @@ describe("signSrpSession", () => { expect(sessionSigned).toEqual(expected); }); + it("should create the correct signed SRP session for a unhashed password", () => { + const session = mockSrpSessionFactory({ password: "Qwerty1!", isHashed: false }); + const response = mockInitiateAuthResponseFactory(); + const sessionSigned = signSrpSession(session, response); + const expected = mockSrpSessionSignedFactory({ password: "Qwerty1!", isHashed: false }); + expect(sessionSigned).toEqual(expected); + }); + it.each(Object.values(positiveSessions))( "should create a signed SRP session with the correct format: session %#", (session) => { @@ -41,7 +49,7 @@ describe("signSrpSession", () => { const { SRP_B, SALT, SECRET_BLOCK } = response.ChallengeParameters ?? {}; // previous session values should remain the same expect(sessionSigned.username).toMatch(session.username); - expect(sessionSigned.passwordHash).toMatch(session.passwordHash); + expect(sessionSigned.password).toMatch(session.password); expect(sessionSigned.poolIdAbbr).toMatch(session.poolIdAbbr); expect(sessionSigned.timestamp).toMatch(session.timestamp); expect(sessionSigned.smallA).toMatch(session.smallA); @@ -63,7 +71,7 @@ describe("signSrpSession", () => { const { SRP_B, SALT, SECRET_BLOCK } = response.ChallengeParameters ?? {}; // previous session values should remain the same expect(sessionSigned.username).toMatch(session.username); - expect(sessionSigned.passwordHash).toMatch(session.passwordHash); + expect(sessionSigned.password).toMatch(session.password); expect(sessionSigned.poolIdAbbr).toMatch(session.poolIdAbbr); expect(sessionSigned.timestamp).toMatch(session.timestamp); expect(sessionSigned.smallA).toMatch(session.smallA); diff --git a/src/cognito-srp-helper.ts b/src/cognito-srp-helper.ts index a9eb7c1..dd1e03a 100644 --- a/src/cognito-srp-helper.ts +++ b/src/cognito-srp-helper.ts @@ -101,22 +101,22 @@ const createTimestamp = (): string => { return `${weekDay} ${month} ${day} ${time} UTC ${year}`; }; -export const createSecretHash = (username: string, clientId: string, secretId: string): string => { - const hmac = CryptoJS.HmacSHA256(`${username}${clientId}`, secretId); +export const createSecretHash = (userId: string, clientId: string, secretId: string): string => { + const hmac = CryptoJS.HmacSHA256(`${userId}${clientId}`, secretId); const secretHash = hmac.toString(CryptoJS.enc.Base64); return secretHash; }; -export const createPasswordHash = (username: string, password: string, poolId: string): string => { +export const createPasswordHash = (userId: string, password: string, poolId: string): string => { const poolIdAbbr = poolId.split("_")[1]; - const usernamePassword = `${poolIdAbbr}${username}:${password}`; + const usernamePassword = `${poolIdAbbr}${userId}:${password}`; const passwordHash = hash(usernamePassword); return passwordHash; }; -export const createSrpSession = (username: string, passwordHash: string, poolId: string): SrpSession => { +export const createSrpSession = (username: string, password: string, poolId: string, isHashed = true): SrpSession => { const poolIdAbbr = poolId.split("_")[1]; const timestamp = createTimestamp(); const smallA = generateSmallA(); @@ -124,8 +124,10 @@ export const createSrpSession = (username: string, passwordHash: string, poolId: return { username, + poolId, poolIdAbbr, - passwordHash, + password, + isHashed, timestamp, smallA: smallA.toString(16), largeA: largeA.toString(16), @@ -139,12 +141,20 @@ export const signSrpSession = (session: SrpSession, response: InitiateAuthRespon if (!response.ChallengeParameters.SECRET_BLOCK) throw new MissingSecretError(); if (!response.ChallengeParameters.SRP_B) throw new MissingLargeBError(); - const { SALT: salt, SECRET_BLOCK: secret, SRP_B: largeB } = response.ChallengeParameters; - const { username, poolIdAbbr, passwordHash, timestamp, smallA, largeA } = session; + const { + SALT: salt, + SECRET_BLOCK: secret, + SRP_B: largeB, + USER_ID_FOR_SRP: userIdForSrp, + } = response.ChallengeParameters; + const { poolId, poolIdAbbr, password, isHashed, timestamp, smallA, largeA } = session; // Check server public key isn't 0 if (largeB.replace(/^0+/, "") === "") throw new AbortOnZeroBSrpError(); + // Hash the password if it isn't already hashed + const passwordHash = isHashed ? password : createPasswordHash(userIdForSrp, password, poolId); + const u = calculateU(new BigInteger(largeA, 16), new BigInteger(largeB, 16)); const x = calculateX(new BigInteger(salt, 16), passwordHash); const s = calculateS(x, new BigInteger(largeB, 16), new BigInteger(smallA, 16), u); @@ -154,7 +164,7 @@ export const signSrpSession = (session: SrpSession, response: InitiateAuthRespon const message = CryptoJS.lib.WordArray.create( Buffer.concat([ Buffer.from(poolIdAbbr, "utf8"), - Buffer.from(username, "utf8"), + Buffer.from(userIdForSrp, "utf8"), Buffer.from(secret, "base64"), Buffer.from(timestamp, "utf8"), ]), diff --git a/src/types.ts b/src/types.ts index 453c388..a0ecea6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,7 +154,10 @@ export type RespondToAuthChallengeRequest = * Credentials needed for SRP authentication. */ export type Credentials = { + sub: string; username: string; + email: string; + phone: string; password: string; poolId: string; clientId: string; @@ -171,8 +174,12 @@ export type Credentials = { export type SrpSession = { /** Username of the user. It is stored here for convenience when passing parameters into `computePasswordSignature` */ username: string; - /** Password hash generated using the users credentials */ - passwordHash: string; + /** Password used for authentication */ + password: string; + /** Flag indicating whether the password has already been hashed */ + isHashed: boolean; + /** Full un-abbreviated ID of the Cognito Userpool. Here it is the full ID that's used e.g. 'eu-west-2_abc123' */ + poolId: string; /** Abbreviated ID of the Cognito Userpool. Here it is just the succeeding ID that's used e.g. 'eu-west-2_abc123' becomes 'abc123' */ poolIdAbbr: string; /** Timestamp captured in the format requiree for Cogntio */