Skip to content

Commit

Permalink
feat: support device SRP challenge (#44)
Browse files Browse the repository at this point in the history
* wip

* initial working demo

* logic to handle device SRP

* unit tests for device srp flow

* initial working integration test for device srp flow (at least for sdk v3)

* initial working integrations tests

* update docs

* fix access to gh secrets

* eslint formatting

* rm demo package

* mv totp-generator from dependencies to dev-dependencies since its only used in tests
  • Loading branch information
simonmcallister0210 committed Nov 2, 2024
1 parent 284326a commit ce195c3
Show file tree
Hide file tree
Showing 22 changed files with 2,923 additions and 492 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: test

on:
pull_request:
branches:
- main
push:
workflow_dispatch:

jobs:
Expand Down
196 changes: 140 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,91 +28,141 @@ Here is an example of how you would use the helper to implement SRP authenticati
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) => {
// . . .
});
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,
},
}),
),
)
.catch((err) => {
// . . .
});

// . . . return login tokens from respondToAuthChallengeResponse
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 the helper to implement SRP authentication with Cognito using the AWS JavaScript SDK v2 (deprecated) using a pre-hashed password:
Here is an example of how you would use createDeviceVerifier to confirm a device:

```ts
// 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:

```ts
// . . . 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);
// Initiate signin with username and password

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

const initiateAuthRes = await cognitoIdentityServiceProvider
.initiateAuth(
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,
DEVICE_KEY: DeviceKey, // Fetch this from client storage
},
}),
)
.promise()
.catch((err) => {
throw err;
});
),
);

// Respond to PASSWORD_VERIFIER challenge

const signedSrpSession = signSrpSession(srpSession, initiateAuthRes);

const respondToAuthChallengeRes = await cognitoIdentityServiceProvider
.respondToAuthChallenge(
const respondToAuthChallengeRes1 = await cognitoIdentityProviderClient.send(
new RespondToAuthChallengeCommand(
wrapAuthChallenge(signedSrpSession, {
ClientId: CLIENT_ID,
ClientId: clientId,
ChallengeName: "PASSWORD_VERIFIER",
ChallengeResponses: {
SECRET_HASH: secretHash,
USERNAME: username,
DEVICE_KEY: DeviceKey,
},
Session: initiateAuthRes.Session,
}),
)
.promise()
.catch((err) => {
throw err;
});
),
);

// . . . return login tokens from respondToAuthChallengeResponse
// 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
Expand Down Expand Up @@ -177,6 +227,20 @@ _SrpSession_ - Client SRP session object containing user credentials and session

---

### `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
Expand All @@ -193,6 +257,26 @@ _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
Expand Down
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"prettier": "^3.0.2",
"randexp": "^0.5.3",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
"typescript": "^4.8.4",
"totp-generator": "^1.0.0"
},
"dependencies": {
"@aws-sdk/client-cognito-identity-provider": "^3.433.0",
Expand Down
53 changes: 53 additions & 0 deletions src/__tests__/integration/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CognitoIdentityProviderClient, SignUpCommand } from "@aws-sdk/client-cognito-identity-provider";
import { CognitoIdentityServiceProvider } from "aws-sdk";

import { createSecretHash } from "../../cognito-srp-helper";

type SignupOptionsV2 = {
username: string;
password: string;
cognitoIdentityServiceProvider: CognitoIdentityServiceProvider;
clientId: string;
secretId: string;
};

export const signupV2 = async (options: SignupOptionsV2) => {
const { username, password, cognitoIdentityServiceProvider, clientId, secretId } = options;
const secretHash = createSecretHash(username, clientId, secretId);

return cognitoIdentityServiceProvider
.signUp({
ClientId: clientId,
Username: username,
Password: password,
SecretHash: secretHash,
})
.promise();
};

type SignupOptionsV3 = {
username: string;
password: string;
cognitoIdentityProviderClient: CognitoIdentityProviderClient;
clientId: string;
secretId: string;
};

export const signupV3 = async (options: SignupOptionsV3) => {
const { username, password, cognitoIdentityProviderClient, clientId, secretId } = options;
const secretHash = createSecretHash(username, clientId, secretId);

await cognitoIdentityProviderClient
.send(
// There's a pre-signup trigger to auto-confirm new users, so no need to Confirm post signup
new SignUpCommand({
ClientId: clientId,
Username: username,
Password: password,
SecretHash: secretHash,
}),
)
.catch((err) => {
throw err;
});
};
Loading

0 comments on commit ce195c3

Please sign in to comment.