You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A Sign-in-with-Slack OIDC flow that runs from the user's account settings and writes one identity_link row on success. The flow:
User clicks "Link Slack account" in account settings.
Webapp redirects to Slack's OIDC authorization endpoint with scope=openid,email,profile and Hephaestus's registered Slack client_id.
Slack redirects back to /oauth/callback/slack-link with the OIDC code + state.
Server-side callback validates the state, exchanges the code for an OIDC id_token, extracts (team_id, sub), and calls IdentityLinkProvider.link(currentUser.id, SLACK, team_id, sub, OIDC).
On success: the webapp returns the user to account settings with a success toast. On failure: a structured-error page with the rejection reason.
This is a link flow, not a login. The user must already be authenticated to Hephaestus when initiating; the callback uses the existing session.
Why
Every Slack-side delivery surface (DMs from the mentor, channel-monitoring feedback, App Home greetings) needs to resolve "the Hephaestus user behind this Slack user-id." OIDC is the cheapest verified-identity flow Slack offers; the alternative (admin bulk-binding) does not scale and removes user consent. The flow's two safety properties — no email auto-link, explicit user-initiated consent — fall out of the OIDC choice naturally.
Acceptance criteria
An account-settings UI section exists with a "Link Slack account" button when no Slack link exists for the user, and a "Unlink" affordance when one exists
The OIDC redirect uses Slack's published OIDC endpoints; the state parameter is generated server-side and bound to the user's session (CSRF protection)
The callback validates state, exchanges the code, extracts (team_id, sub), and writes via IdentityLinkProvider.link(...) with linkedVia=OIDC
A failed exchange (invalid code, expired state, OIDC error) returns a structured-error page; no partial identity_link row is written
An attempt to link an already-linked (provider=SLACK, team_id, sub) is handled by #1240 — this sub-issue surfaces the takeover-protection path's error message
An integration test (with a stubbed Slack OIDC endpoint) walks the happy path; a second test walks the state-CSRF rejection path
An ArchUnit test rejects new email-based linking logic; the link(...) SPI call accepts external_id, not email, and the OIDC callback uses sub (Slack's stable user id), not email
Tests to write
Integration tests for happy path + state-mismatch + already-linked rejection.
Unit test on state generation: state is cryptographically random, bound to session, single-use.
A regression test asserting the callback does not read or persist the user's Slack email anywhere (Slack OIDC returns email, but Hephaestus does not store it from this flow).
Webapp Storybook story for the account-settings link section (per the project memory's UI-test preference).
Implementation notes
The Slack OIDC scope is openid,email,profile per Slack's docs. We request email because Slack requires it for OIDC, but we discard it in the callback — explicit non-goal per the IDL epic.
The state parameter must be single-use; replaying a successful callback's state must fail. Standard CSRF defense.
The Slack client_id + client_secret for OIDC are workspace-independent platform credentials (one Slack app for Hephaestus, used by all installations); they live in application.yaml config, not in workspace_integration.
The webapp UI section reuses existing settings-page components; no new design system primitives are needed.
Part of #1200.
What ships
A Sign-in-with-Slack OIDC flow that runs from the user's account settings and writes one
identity_linkrow on success. The flow:scope=openid,email,profileand Hephaestus's registered Slack client_id./oauth/callback/slack-linkwith the OIDC code + state.(team_id, sub), and callsIdentityLinkProvider.link(currentUser.id, SLACK, team_id, sub, OIDC).This is a link flow, not a login. The user must already be authenticated to Hephaestus when initiating; the callback uses the existing session.
Why
Every Slack-side delivery surface (DMs from the mentor, channel-monitoring feedback, App Home greetings) needs to resolve "the Hephaestus user behind this Slack user-id." OIDC is the cheapest verified-identity flow Slack offers; the alternative (admin bulk-binding) does not scale and removes user consent. The flow's two safety properties — no email auto-link, explicit user-initiated consent — fall out of the OIDC choice naturally.
Acceptance criteria
(team_id, sub), and writes viaIdentityLinkProvider.link(...)withlinkedVia=OIDCidentity_linkrow is written(provider=SLACK, team_id, sub)is handled by#1240— this sub-issue surfaces the takeover-protection path's error messagelink(...)SPI call acceptsexternal_id, not email, and the OIDC callback usessub(Slack's stable user id), notemailTests to write
Implementation notes
scopeisopenid,email,profileper Slack's docs. We request email because Slack requires it for OIDC, but we discard it in the callback — explicit non-goal per the IDL epic.application.yamlconfig, not inworkspace_integration.Dependencies
Depends on #1237. Blocks #1239. Blocks #1240.