-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): getUserAttributeVerificationCode full support
- Loading branch information
Showing
5 changed files
with
301 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
integration-tests/aws-sdk/getUserAttributeVerificationCode.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { UUID } from "../../src/__tests__/patterns"; | ||
import { TestContext } from "../../src/__tests__/testContext"; | ||
import { withCognitoSdk } from "./setup"; | ||
import { User } from "../../src/services/userPoolService"; | ||
|
||
describe( | ||
"CognitoIdentityServiceProvider.getUserAttributeVerificationCode", | ||
withCognitoSdk((Cognito, DataStoreFactory) => { | ||
it("sends a verification code for a user's attribute", async () => { | ||
const client = Cognito(); | ||
|
||
const pool = await client | ||
.createUserPool({ | ||
PoolName: "test", | ||
AutoVerifiedAttributes: ["email"], | ||
}) | ||
.promise(); | ||
const userPoolId = pool.UserPool?.Id as string; | ||
|
||
const upc = await client | ||
.createUserPoolClient({ | ||
UserPoolId: userPoolId, | ||
ClientName: "test", | ||
}) | ||
.promise(); | ||
|
||
await client | ||
.adminCreateUser({ | ||
UserAttributes: [{ Name: "email", Value: "[email protected]" }], | ||
Username: "abc", | ||
UserPoolId: userPoolId, | ||
TemporaryPassword: "def", | ||
DesiredDeliveryMediums: ["EMAIL"], | ||
}) | ||
.promise(); | ||
|
||
await client | ||
.adminConfirmSignUp({ | ||
UserPoolId: userPoolId, | ||
Username: "abc", | ||
}) | ||
.promise(); | ||
|
||
// login as the user | ||
const initiateAuthResponse = await client | ||
.initiateAuth({ | ||
AuthFlow: "USER_PASSWORD_AUTH", | ||
AuthParameters: { | ||
USERNAME: "abc", | ||
PASSWORD: "def", | ||
}, | ||
ClientId: upc.UserPoolClient?.ClientId as string, | ||
}) | ||
.promise(); | ||
|
||
await client | ||
.getUserAttributeVerificationCode({ | ||
AccessToken: initiateAuthResponse.AuthenticationResult | ||
?.AccessToken as string, | ||
AttributeName: "email", | ||
}) | ||
.promise(); | ||
|
||
// get the user's code -- this is very nasty | ||
const ds = await DataStoreFactory().create(TestContext, userPoolId, {}); | ||
const storedUser = (await ds.get(TestContext, ["Users", "abc"])) as User; | ||
|
||
expect(storedUser.AttributeVerificationCode).toMatch(/^\d{4}$/); | ||
}); | ||
}) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import jwt from "jsonwebtoken"; | ||
import * as uuid from "uuid"; | ||
import { newMockCognitoService } from "../__tests__/mockCognitoService"; | ||
import { newMockMessages } from "../__tests__/mockMessages"; | ||
import { newMockUserPoolService } from "../__tests__/mockUserPoolService"; | ||
import { TestContext } from "../__tests__/testContext"; | ||
import { InvalidParameterError, UserNotFoundError } from "../errors"; | ||
import PrivateKey from "../keys/cognitoLocal.private.json"; | ||
import { Messages, UserPoolService } from "../services"; | ||
import { attribute, attributeValue } from "../services/userPoolService"; | ||
import { | ||
GetUserAttributeVerificationCode, | ||
GetUserAttributeVerificationCodeTarget, | ||
} from "./getUserAttributeVerificationCode"; | ||
import * as TDB from "../__tests__/testDataBuilder"; | ||
|
||
const validToken = jwt.sign( | ||
{ | ||
sub: "0000-0000", | ||
event_id: "0", | ||
token_use: "access", | ||
scope: "aws.cognito.signin.user.admin", | ||
auth_time: new Date(), | ||
jti: uuid.v4(), | ||
client_id: "test", | ||
username: "0000-0000", | ||
}, | ||
PrivateKey.pem, | ||
{ | ||
algorithm: "RS256", | ||
issuer: `http://localhost:9229/test`, | ||
expiresIn: "24h", | ||
keyid: "CognitoLocal", | ||
} | ||
); | ||
|
||
describe("GetUserAttributeVerificationCode target", () => { | ||
let getUserAttributeVerificationCode: GetUserAttributeVerificationCodeTarget; | ||
let mockUserPoolService: jest.Mocked<UserPoolService>; | ||
let mockMessages: jest.Mocked<Messages>; | ||
|
||
beforeEach(() => { | ||
mockUserPoolService = newMockUserPoolService({ | ||
Id: "test", | ||
AutoVerifiedAttributes: ["email"], | ||
}); | ||
mockMessages = newMockMessages(); | ||
getUserAttributeVerificationCode = GetUserAttributeVerificationCode({ | ||
cognito: newMockCognitoService(mockUserPoolService), | ||
messages: mockMessages, | ||
otp: () => "1234", | ||
}); | ||
}); | ||
|
||
it("throws if token isn't valid", async () => { | ||
await expect( | ||
getUserAttributeVerificationCode(TestContext, { | ||
AccessToken: "blah", | ||
AttributeName: "email", | ||
}) | ||
).rejects.toBeInstanceOf(InvalidParameterError); | ||
}); | ||
|
||
it("throws if user doesn't exist", async () => { | ||
mockUserPoolService.getUserByUsername.mockResolvedValue(null); | ||
|
||
await expect( | ||
getUserAttributeVerificationCode(TestContext, { | ||
AccessToken: validToken, | ||
AttributeName: "email", | ||
}) | ||
).rejects.toEqual(new UserNotFoundError()); | ||
}); | ||
|
||
it("throws if the user doesn't have a valid way to contact them", async () => { | ||
const user = TDB.user({ | ||
Attributes: [], | ||
}); | ||
|
||
mockUserPoolService.getUserByUsername.mockResolvedValue(user); | ||
|
||
await expect( | ||
getUserAttributeVerificationCode(TestContext, { | ||
ClientMetadata: { | ||
client: "metadata", | ||
}, | ||
AccessToken: validToken, | ||
AttributeName: "email", | ||
}) | ||
).rejects.toEqual( | ||
new InvalidParameterError( | ||
"User has no attribute matching desired auto verified attributes" | ||
) | ||
); | ||
}); | ||
|
||
it("delivers a OTP code to the user", async () => { | ||
const user = TDB.user({ | ||
Attributes: [attribute("email", "[email protected]")], | ||
}); | ||
|
||
mockUserPoolService.getUserByUsername.mockResolvedValue(user); | ||
|
||
await getUserAttributeVerificationCode(TestContext, { | ||
ClientMetadata: { | ||
client: "metadata", | ||
}, | ||
AccessToken: validToken, | ||
AttributeName: "email", | ||
}); | ||
|
||
expect(mockMessages.deliver).toHaveBeenCalledWith( | ||
TestContext, | ||
"VerifyUserAttribute", | ||
null, | ||
"test", | ||
user, | ||
"1234", | ||
{ client: "metadata" }, | ||
{ | ||
AttributeName: "email", | ||
DeliveryMedium: "EMAIL", | ||
Destination: attributeValue("email", user.Attributes), | ||
} | ||
); | ||
|
||
expect(mockUserPoolService.saveUser).toHaveBeenCalledWith( | ||
TestContext, | ||
expect.objectContaining({ | ||
AttributeVerificationCode: "1234", | ||
}) | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { | ||
GetUserAttributeVerificationCodeRequest, | ||
GetUserAttributeVerificationCodeResponse, | ||
} from "aws-sdk/clients/cognitoidentityserviceprovider"; | ||
import jwt from "jsonwebtoken"; | ||
import { Messages, Services, UserPoolService } from "../services"; | ||
import { InvalidParameterError, UserNotFoundError } from "../errors"; | ||
import { selectAppropriateDeliveryMethod } from "../services/messageDelivery/deliveryMethod"; | ||
import { Token } from "../services/tokenGenerator"; | ||
import { User } from "../services/userPoolService"; | ||
import { Context, Target } from "./router"; | ||
|
||
const sendAttributeVerificationCode = async ( | ||
ctx: Context, | ||
userPool: UserPoolService, | ||
user: User, | ||
messages: Messages, | ||
req: GetUserAttributeVerificationCodeRequest, | ||
code: string | ||
) => { | ||
const deliveryDetails = selectAppropriateDeliveryMethod( | ||
userPool.config.AutoVerifiedAttributes ?? [], | ||
user | ||
); | ||
if (!deliveryDetails) { | ||
// TODO: I don't know what the real error message should be for this | ||
throw new InvalidParameterError( | ||
"User has no attribute matching desired auto verified attributes" | ||
); | ||
} | ||
|
||
await messages.deliver( | ||
ctx, | ||
"VerifyUserAttribute", | ||
null, | ||
userPool.config.Id, | ||
user, | ||
code, | ||
req.ClientMetadata, | ||
deliveryDetails | ||
); | ||
}; | ||
|
||
export type GetUserAttributeVerificationCodeTarget = Target< | ||
GetUserAttributeVerificationCodeRequest, | ||
GetUserAttributeVerificationCodeResponse | ||
>; | ||
|
||
type GetUserAttributeVerificationCodeServices = Pick< | ||
Services, | ||
"cognito" | "otp" | "messages" | ||
>; | ||
|
||
export const GetUserAttributeVerificationCode = | ||
({ | ||
cognito, | ||
otp, | ||
messages, | ||
}: GetUserAttributeVerificationCodeServices): GetUserAttributeVerificationCodeTarget => | ||
async (ctx, req) => { | ||
const decodedToken = jwt.decode(req.AccessToken) as Token | null; | ||
if (!decodedToken) { | ||
ctx.logger.info("Unable to decode token"); | ||
throw new InvalidParameterError(); | ||
} | ||
|
||
const userPool = await cognito.getUserPoolForClientId( | ||
ctx, | ||
decodedToken.client_id | ||
); | ||
const user = await userPool.getUserByUsername(ctx, decodedToken.sub); | ||
if (!user) { | ||
throw new UserNotFoundError(); | ||
} | ||
|
||
const code = otp(); | ||
|
||
await userPool.saveUser(ctx, { | ||
...user, | ||
AttributeVerificationCode: code, | ||
}); | ||
|
||
await sendAttributeVerificationCode( | ||
ctx, | ||
userPool, | ||
user, | ||
messages, | ||
req, | ||
code | ||
); | ||
|
||
return {}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters