Skip to content

feat(server,webapp): Sign in with Slack OIDC link flow #1238

@FelixTJDietrich

Description

@FelixTJDietrich

Part of #1200.

What ships

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:

  1. User clicks "Link Slack account" in account settings.
  2. Webapp redirects to Slack's OIDC authorization endpoint with scope=openid,email,profile and Hephaestus's registered Slack client_id.
  3. Slack redirects back to /oauth/callback/slack-link with the OIDC code + state.
  4. 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).
  5. 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.

Dependencies

Depends on #1237. Blocks #1239. Blocks #1240.

Metadata

Metadata

Assignees

No one assigned

    Labels

    application-serverSpring Boot server: APIs, business logic, databasefeatureNew feature or enhancementpriority:highAddress this sprint - Significant impactwebappReact app: UI components, routes, state management

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions