Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): adding in admin respond to auth challenge target and handling adminInitiateAuth SMS_MFA #332

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog
| AdminListUserAuthEvents | ❌ |
| AdminRemoveUserFromGroup | ✅ |
| AdminResetUserPassword | ❌ |
| AdminRespondToAuthChallenge | |
| AdminRespondToAuthChallenge | 🕒 (partial support) |
| AdminSetUserMFAPreference | ❌ |
| AdminSetUserPassword | ✅ |
| AdminSetUserSettings | |
| AdminSetUserSettings | |
| AdminUpdateAuthEventFeedback | ❌ |
| AdminUpdateDeviceStatus | ❌ |
| AdminUpdateUserAttributes | ✅ |
Expand Down
9 changes: 9 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export class CodeMismatchError extends CognitoError {
}
}

export class MFAMethodNotFoundException extends CognitoError {
public constructor() {
super(
"MFAMethodNotFoundException",
"No MFA mechanism registered in the account"
);
}
}

export class InvalidPasswordError extends CognitoError {
public constructor() {
super("InvalidPasswordException", "Invalid password");
Expand Down
4 changes: 2 additions & 2 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ export const createServer = (

req.log.error(`Cognito Local unsupported feature: ${ex.message}`);
res.status(500).json({
code: "CognitoLocal#Unsupported",
__type: "CognitoLocal#Unsupported",
message: `Cognito Local unsupported feature: ${ex.message}`,
});
return;
} else if (ex instanceof CognitoError) {
req.log.warn(ex, `Error handling target: ${target}`);
res.status(400).json({
code: ex.code,
__type: ex.code,
message: ex.message,
});
return;
Expand Down
8 changes: 2 additions & 6 deletions src/targets/addCustomAttributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { newMockCognitoService } from "../__tests__/mockCognitoService";
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
import { TestContext } from "../__tests__/testContext";
import * as TDB from "../__tests__/testDataBuilder";
import {
GroupNotFoundError,
InvalidParameterError,
UserNotFoundError,
} from "../errors";
import { CognitoService, UserPoolService } from "../services";
import { InvalidParameterError } from "../errors";
import { CognitoService } from "../services";
import {
AddCustomAttributes,
AddCustomAttributesTarget,
Expand Down
224 changes: 223 additions & 1 deletion src/targets/adminInitiateAuth.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { newMockCognitoService } from "../__tests__/mockCognitoService";
import { newMockMessages } from "../__tests__/mockMessages";
import { newMockTokenGenerator } from "../__tests__/mockTokenGenerator";
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
import { newMockTriggers } from "../__tests__/mockTriggers";
import { TestContext } from "../__tests__/testContext";
import * as TDB from "../__tests__/testDataBuilder";
import { CognitoService, Triggers, UserPoolService } from "../services";
import { NotAuthorizedError } from "../errors";
import {
CognitoService,
Messages,
Triggers,
UserPoolService,
} from "../services";
import { TokenGenerator } from "../services/tokenGenerator";
import { User } from "../services/userPoolService";
import {
AdminInitiateAuth,
AdminInitiateAuthTarget,
Expand All @@ -16,6 +24,8 @@ describe("AdminInitiateAuth target", () => {

let mockCognitoService: jest.Mocked<CognitoService>;
let mockTokenGenerator: jest.Mocked<TokenGenerator>;
let mockMessages: jest.Mocked<Messages>;
let mockOtp: jest.MockedFunction<() => string>;
let mockTriggers: jest.Mocked<Triggers>;
let mockUserPoolService: jest.Mocked<UserPoolService>;
const userPoolClient = TDB.appClient();
Expand All @@ -24,13 +34,17 @@ describe("AdminInitiateAuth target", () => {
mockUserPoolService = newMockUserPoolService({
Id: userPoolClient.UserPoolId,
});
mockMessages = newMockMessages();
mockOtp = jest.fn().mockReturnValue("123456");
mockCognitoService = newMockCognitoService(mockUserPoolService);
mockCognitoService.getAppClient.mockResolvedValue(userPoolClient);
mockTriggers = newMockTriggers();
mockTokenGenerator = newMockTokenGenerator();
adminInitiateAuth = AdminInitiateAuth({
triggers: mockTriggers,
cognito: mockCognitoService,
messages: mockMessages,
otp: mockOtp,
tokenGenerator: mockTokenGenerator,
});
});
Expand Down Expand Up @@ -131,4 +145,212 @@ describe("AdminInitiateAuth target", () => {
"RefreshTokens"
);
});

describe("when password matches", () => {
describe("when MFA is ON", () => {
beforeEach(() => {
mockUserPoolService.options.MfaConfiguration = "ON";
});

describe("when user has SMS_MFA configured", () => {
let user: User;

beforeEach(() => {
user = TDB.user({
Attributes: [
{
Name: "phone_number",
Value: "0411000111",
},
],
MFAOptions: [
{
DeliveryMedium: "SMS",
AttributeName: "phone_number",
},
],
});
mockUserPoolService.getUserByUsername.mockResolvedValue(user);
});

it("sends MFA code to user", async () => {
const output = await adminInitiateAuth(TestContext, {
ClientId: userPoolClient.ClientId,
UserPoolId: userPoolClient.UserPoolId,
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: user.Username,
PASSWORD: user.Password,
},
});

expect(output).toBeDefined();

expect(mockMessages.deliver).toHaveBeenCalledWith(
TestContext,
"Authentication",
userPoolClient.ClientId,
userPoolClient.UserPoolId,
user,
"123456",
undefined,
{
AttributeName: "phone_number",
DeliveryMedium: "SMS",
Destination: "0411000111",
}
);

// also saves the code on the user for comparison later
expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(
TestContext,
{
...user,
MFACode: "123456",
}
);
});
});

describe("when user doesn't have MFA configured", () => {
const user = TDB.user({ MFAOptions: undefined });

beforeEach(() => {
mockUserPoolService.getUserByUsername.mockResolvedValue(user);
});

it("throws an exception", async () => {
await expect(
adminInitiateAuth(TestContext, {
ClientId: userPoolClient.ClientId,
UserPoolId: userPoolClient.UserPoolId,
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: user.Username,
PASSWORD: user.Password,
},
})
).rejects.toBeInstanceOf(NotAuthorizedError);
});
});
});

describe("when MFA is OPTIONAL", () => {
beforeEach(() => {
mockUserPoolService.options.MfaConfiguration = "OPTIONAL";
});

describe("when user has SMS_MFA configured", () => {
let user: User;

beforeEach(() => {
user = TDB.user({
Attributes: [
{
Name: "phone_number",
Value: "0411000111",
},
],
MFAOptions: [
{
DeliveryMedium: "SMS",
AttributeName: "phone_number",
},
],
});
mockUserPoolService.getUserByUsername.mockResolvedValue(user);
});

it("sends MFA code to user", async () => {
const output = await adminInitiateAuth(TestContext, {
ClientId: userPoolClient.ClientId,
UserPoolId: userPoolClient.UserPoolId,
ClientMetadata: {
client: "metadata",
},
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: user.Username,
PASSWORD: user.Password,
},
});

expect(output).toBeDefined();

expect(mockMessages.deliver).toHaveBeenCalledWith(
TestContext,
"Authentication",
userPoolClient.ClientId,
userPoolClient.UserPoolId,
user,
"123456",
{
client: "metadata",
},
{
AttributeName: "phone_number",
DeliveryMedium: "SMS",
Destination: "0411000111",
}
);

// also saves the code on the user for comparison later
expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(
TestContext,
{
...user,
MFACode: "123456",
}
);
});
});

describe("when user doesn't have MFA configured", () => {
const user = TDB.user({
MFAOptions: undefined,
});

beforeEach(() => {
mockUserPoolService.getUserByUsername.mockResolvedValue(user);
});

it("generates tokens", async () => {
mockTokenGenerator.generate.mockResolvedValue({
AccessToken: "access",
IdToken: "id",
RefreshToken: "refresh",
});
mockUserPoolService.listUserGroupMembership.mockResolvedValue([]);

const output = await adminInitiateAuth(TestContext, {
ClientId: userPoolClient.ClientId,
UserPoolId: userPoolClient.UserPoolId,
AuthFlow: "ADMIN_USER_PASSWORD_AUTH",
AuthParameters: {
USERNAME: user.Username,
PASSWORD: user.Password,
},
ClientMetadata: {
client: "metadata",
},
});

expect(output).toBeDefined();

expect(output.AuthenticationResult?.AccessToken).toEqual("access");
expect(output.AuthenticationResult?.IdToken).toEqual("id");
expect(output.AuthenticationResult?.RefreshToken).toEqual("refresh");

expect(mockTokenGenerator.generate).toHaveBeenCalledWith(
TestContext,
user,
[],
userPoolClient,
{ client: "metadata" },
"Authentication"
);
});
});
});
});
});
Loading