Skip to content
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
2 changes: 1 addition & 1 deletion lambda/user-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ When both hold, it:
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;**
2. generates a 20-character password containing all four character classes;
3. sets it with `UpdateLoginProfile` and `PasswordResetRequired: true`, so the user must replace it at first sign-in;
4. DMs the user, as the Slack app, with the sign-in page, their IAM user name and the temporary password.
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.

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.

Expand Down
13 changes: 11 additions & 2 deletions lambda/user-bot/src/message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// The incubator account's sign-in page, via its alias hfla-incubator.
export const SIGN_IN_URL = "https://hfla-incubator.signin.aws.amazon.com/console";

// Opens the "Assign MFA device" wizard for whoever is signed in, so one link works for
// every user. It only works once signed in: the route is in the #fragment, which the
// browser never sends to AWS, so a signed-out visit loses it on the way through sign-in
// and lands on the console home page. Hence the "once signed in" in the message.
// Tested by hand 2026-10-04; AWS does not document console routes and may change them.
export const MFA_SETUP_URL = "https://console.aws.amazon.com/iam/home#/security_credentials/mfa";

// Slack mrkdwn. The user name and password are in inline code so that characters
// such as * and _ are shown literally rather than read as formatting.
export function buildWelcomeMessage(userName: string, password: string): string {
Expand All @@ -12,8 +19,10 @@ export function buildWelcomeMessage(userName: string, password: string): string
`*Temporary password:* \`${password}\``,
"",
"The first time you sign in you will be asked to replace this password with one of your own. " +
"Then set up multi-factor authentication (MFA) from *Security credentials* in the account " +
"menu: most of your access only works once MFA is set up.",
"Then, in the same browser, set up multi-factor authentication (MFA): most of your access " +
"only works once MFA is set up.",
"",
`*Set up MFA (once signed in):* ${MFA_SETUP_URL}`,
"",
"If the password does not work, ask the DevOps team on Slack for a new one.",
].join("\n");
Expand Down
3 changes: 2 additions & 1 deletion lambda/user-bot/test/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { mockClient } from "aws-sdk-client-mock";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { createHandler, type Dependencies } from "../src/handler";
import { SIGN_IN_URL } from "../src/message";
import { MFA_SETUP_URL, SIGN_IN_URL } from "../src/message";
import { generatePassword } from "../src/password";
import type { DirectMessage, MessageSender } from "../src/senders/types";
import { captureLogger, createLoginProfileEvent, TEST_PASSWORD, TEST_SLACK_ID } from "./helpers";
Expand Down Expand Up @@ -58,6 +58,7 @@ describe("happy path", () => {
expect(sent[0]!.text).toContain(TEST_PASSWORD);
expect(sent[0]!.text).toContain("new.member");
expect(sent[0]!.text).toContain(SIGN_IN_URL);
expect(sent[0]!.text).toContain(MFA_SETUP_URL);
expect(logger.text()).not.toContain(TEST_PASSWORD);
});

Expand Down
24 changes: 24 additions & 0 deletions lambda/user-bot/test/message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";

import { buildWelcomeMessage, MFA_SETUP_URL, SIGN_IN_URL } from "../src/message";
import { TEST_PASSWORD } from "./helpers";

describe("buildWelcomeMessage", () => {
const text = buildWelcomeMessage("new.member", TEST_PASSWORD);

it("includes the incubator sign-in page, the user name and the password", () => {
expect(SIGN_IN_URL).toBe("https://hfla-incubator.signin.aws.amazon.com/console");
expect(text).toContain(`*Sign-in page:* ${SIGN_IN_URL}`);
expect(text).toContain("*IAM user name:* `new.member`");
expect(text).toContain(`*Temporary password:* \`${TEST_PASSWORD}\``);
});

it("links straight to the signed-in user's Assign MFA device wizard", () => {
expect(MFA_SETUP_URL).toBe("https://console.aws.amazon.com/iam/home#/security_credentials/mfa");
expect(text).toContain(`*Set up MFA (once signed in):* ${MFA_SETUP_URL}`);
});

it("gives the MFA link after the sign-in details, since it only works once signed in", () => {
expect(text.indexOf(MFA_SETUP_URL)).toBeGreaterThan(text.indexOf(TEST_PASSWORD));
});
});
Loading