Skip to content

Admin sign-in and access #29

Closed
Closed

Description

@johnf

Parent

#26

What to build

An admin can sign in to the admin pages, on their own origin (ADMIN_ORIGIN), through our own OIDC provider and see a placeholder page saying who they are and that they're an admin. A user who isn't an admin sees a 403 page showing their sub. Either can sign out, which also ends their session at our provider and leaves them on a signed-out page.

  • /admin/ is registered only when sign-in and ADMIN_ORIGIN are configured; otherwise startup logs a warning. Every admin route answers only on the admin host, and nothing but /admin answers there.
  • The admin pages are a built-in confidential client of our provider (not in OIDC_CLIENTS), with its secret derived from OIDC_COOKIE_KEYS. Sign-in is a server-side authorization code flow with PKCE, state and nonce; the token request reaches the provider in-process, so the API never calls its own public URL.
  • The session is an encrypted cookie holding sub, sign-in time and ID token, lasting 8 hours, SameSite=Lax, scoped to /admin. A short-lived encrypted cookie holds each sign-in's state, verifier, nonce and the page to return to.
  • Every request checks the session's sub against ADMIN_USERS.
  • POSTs check Origin against the admin origin; responses carry a CSP allowing only same-origin scripts and styles, and no framing.
  • Pages are Eta templates rendered with @fastify/view, escaping by default, with a shared layout showing the signed-in user and sign-out.

See #26 for the full decisions.

Acceptance criteria

  • With no session, /admin/ redirects to sign-in and returns to the page asked for afterwards
  • A user in ADMIN_USERS signs in through the fake upstream provider and sees the placeholder page with their name or sub
  • A user already signed in at our provider gets into admin without visiting the upstream provider
  • A user not in ADMIN_USERS gets a 403 page showing their sub and a sign-out button
  • An empty or unset ADMIN_USERS lets nobody in
  • Removing a user from ADMIN_USERS stops their existing session
  • A session older than 8 hours is sent to sign in; a tampered or foreign cookie counts as no session
  • A callback with a wrong state is refused
  • Sign-out clears the session and redirects to the provider's end-session endpoint with id_token_hint, ending on /admin/signed-out
  • A POST with a missing or foreign Origin is refused
  • Responses carry the CSP header; interpolated values are escaped
  • Admin isn't registered when sign-in or ADMIN_ORIGIN isn't configured
  • ADR 0002 written, ADR 0001's consequence about which clients get tokens updated, Admin and Admin pages added to the glossary
  • ADMIN_USERS and ADMIN_ORIGIN documented in the README and .env.example
  • The admin pages answer only on the admin origin, and only /admin answers there

Blocked by

None (can start immediately)

Original request

Parent

#26

What to build

An admin can sign in to the admin pages through our own OIDC provider and see a placeholder page saying who they are and that they're an admin. A user who isn't an admin sees a 403 page showing their sub. Either can sign out, which also ends their session at our provider.

  • /admin/ is registered only when sign-in is configured; otherwise startup logs a warning.
  • The admin pages are a built-in confidential client of our provider (not in OIDC_CLIENTS), with its secret derived from OIDC_COOKIE_KEYS. Sign-in is a server-side authorization code flow with PKCE, state and nonce; the token request reaches the provider in-process, so the API never calls its own public URL.
  • The session is an encrypted cookie holding sub, sign-in time and ID token, lasting 8 hours, SameSite=Lax, scoped to /admin. A short-lived encrypted cookie holds each sign-in's state, verifier, nonce and the page to return to.
  • Every request checks the session's sub against ADMIN_USERS.
  • POSTs check Origin; responses carry a CSP allowing only same-origin scripts and styles, and no framing.
  • Pages use an escaping html tagged-template helper and a shared, unstyled layout showing the signed-in user and sign-out.

See #26 for the full decisions.

Acceptance criteria

  • With no session, /admin/ redirects to sign-in and returns to the page asked for afterwards
  • A user in ADMIN_USERS signs in through the fake upstream provider and sees the placeholder page with their name or sub
  • A user already signed in at our provider gets into admin without visiting the upstream provider
  • A user not in ADMIN_USERS gets a 403 page showing their sub and a sign-out button
  • An empty or unset ADMIN_USERS lets nobody in
  • Removing a user from ADMIN_USERS stops their existing session
  • A session older than 8 hours is sent to sign in; a tampered or foreign cookie counts as no session
  • A callback with a wrong state is refused
  • Sign-out clears the session and redirects to the provider's end-session endpoint with id_token_hint
  • A POST with a missing or foreign Origin is refused
  • Responses carry the CSP header; interpolated values are escaped
  • Admin isn't registered when sign-in isn't configured
  • ADR 0002 written, ADR 0001's consequence about which clients get tokens updated, Admin and Admin pages added to the glossary
  • ADMIN_USERS documented in the README and .env.example

Blocked by

None (can start immediately)

Activity

  1. johnf commented on Oct 28, 2026

    @johnf
    ContributorAuthor

    Updated to match PR #33; the previous text is kept under Original request. Changes: the admin pages are on their own origin, set by a new ADMIN_ORIGIN setting (see #26 for why). Sign-out ends on /admin/signed-out instead of signing straight back in. Pages are rendered as Eta templates via @fastify/view, not with the hand-rolled html helper.

  2. johnf commented on Oct 28, 2026

    @johnf
    ContributorAuthor

    Shipped in PR #33 (merge e2db8c2). All acceptance criteria met.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions