Skip to content

Commit 89cd3a9

Browse files
authored
Link the user-bot welcome DM to the MFA setup wizard (#217)
1 parent d7fb743 commit 89cd3a9

4 files changed

Lines changed: 38 additions & 4 deletions

File tree

‎lambda/user-bot/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ When both hold, it:
2424
1. reads the Slack bot token (see [The Slack token](#the-slack-token)). **If that fails, it stops here and the user's password is not touched;**
2525
2. generates a 20-character password containing all four character classes;
2626
3. sets it with `UpdateLoginProfile` and `PasswordResetRequired: true`, so the user must replace it at first sign-in;
27-
4. DMs the user, as the Slack app, with the sign-in page, their IAM user name and the temporary password.
27+
4. DMs the user, as the Slack app, with the sign-in page, their IAM user name, the temporary password, and a link straight to the **Assign MFA device** wizard for their own user (`MFA_SETUP_URL` in `src/message.ts`). That link only works once they are signed in.
2828

2929
Step 1 comes before the reset on purpose. Reading the token at send time would mean a bad token leaves the user with a new password that nobody received.
3030

‎lambda/user-bot/src/message.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// The incubator account's sign-in page, via its alias hfla-incubator.
22
export const SIGN_IN_URL = "https://hfla-incubator.signin.aws.amazon.com/console";
33

4+
// Opens the "Assign MFA device" wizard for whoever is signed in, so one link works for
5+
// every user. It only works once signed in: the route is in the #fragment, which the
6+
// browser never sends to AWS, so a signed-out visit loses it on the way through sign-in
7+
// and lands on the console home page. Hence the "once signed in" in the message.
8+
// Tested by hand 2026-10-04; AWS does not document console routes and may change them.
9+
export const MFA_SETUP_URL = "https://console.aws.amazon.com/iam/home#/security_credentials/mfa";
10+
411
// Slack mrkdwn. The user name and password are in inline code so that characters
512
// such as * and _ are shown literally rather than read as formatting.
613
export function buildWelcomeMessage(userName: string, password: string): string {
@@ -12,8 +19,10 @@ export function buildWelcomeMessage(userName: string, password: string): string
1219
`*Temporary password:* \`${password}\``,
1320
"",
1421
"The first time you sign in you will be asked to replace this password with one of your own. " +
15-
"Then set up multi-factor authentication (MFA) from *Security credentials* in the account " +
16-
"menu: most of your access only works once MFA is set up.",
22+
"Then, in the same browser, set up multi-factor authentication (MFA): most of your access " +
23+
"only works once MFA is set up.",
24+
"",
25+
`*Set up MFA (once signed in):* ${MFA_SETUP_URL}`,
1726
"",
1827
"If the password does not work, ask the DevOps team on Slack for a new one.",
1928
].join("\n");

‎lambda/user-bot/test/handler.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { mockClient } from "aws-sdk-client-mock";
88
import { beforeEach, describe, expect, it, vi } from "vitest";
99

1010
import { createHandler, type Dependencies } from "../src/handler";
11-
import { SIGN_IN_URL } from "../src/message";
11+
import { MFA_SETUP_URL, SIGN_IN_URL } from "../src/message";
1212
import { generatePassword } from "../src/password";
1313
import type { DirectMessage, MessageSender } from "../src/senders/types";
1414
import { captureLogger, createLoginProfileEvent, TEST_PASSWORD, TEST_SLACK_ID } from "./helpers";
@@ -58,6 +58,7 @@ describe("happy path", () => {
5858
expect(sent[0]!.text).toContain(TEST_PASSWORD);
5959
expect(sent[0]!.text).toContain("new.member");
6060
expect(sent[0]!.text).toContain(SIGN_IN_URL);
61+
expect(sent[0]!.text).toContain(MFA_SETUP_URL);
6162
expect(logger.text()).not.toContain(TEST_PASSWORD);
6263
});
6364

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { buildWelcomeMessage, MFA_SETUP_URL, SIGN_IN_URL } from "../src/message";
4+
import { TEST_PASSWORD } from "./helpers";
5+
6+
describe("buildWelcomeMessage", () => {
7+
const text = buildWelcomeMessage("new.member", TEST_PASSWORD);
8+
9+
it("includes the incubator sign-in page, the user name and the password", () => {
10+
expect(SIGN_IN_URL).toBe("https://hfla-incubator.signin.aws.amazon.com/console");
11+
expect(text).toContain(`*Sign-in page:* ${SIGN_IN_URL}`);
12+
expect(text).toContain("*IAM user name:* `new.member`");
13+
expect(text).toContain(`*Temporary password:* \`${TEST_PASSWORD}\``);
14+
});
15+
16+
it("links straight to the signed-in user's Assign MFA device wizard", () => {
17+
expect(MFA_SETUP_URL).toBe("https://console.aws.amazon.com/iam/home#/security_credentials/mfa");
18+
expect(text).toContain(`*Set up MFA (once signed in):* ${MFA_SETUP_URL}`);
19+
});
20+
21+
it("gives the MFA link after the sign-in details, since it only works once signed in", () => {
22+
expect(text.indexOf(MFA_SETUP_URL)).toBeGreaterThan(text.indexOf(TEST_PASSWORD));
23+
});
24+
});

0 commit comments

Comments
 (0)