Add admin magic-link login and sessions #55

Closed
opened 2026-05-22 11:43:11 -05:00 by erik · 4 comments
Owner

Goal

Implement the initial admin authentication bootstrap used before passkeys exist: an ADMIN_EMAIL allow-list, magic-link login, session cookies, logout, and a protected admin dashboard. This is the first trusted login path; passkeys are registered only after this session exists.

Reference behavior: /home/erik/Private/code/github/evcraddock/erikcraddock.me uses ADMIN_EMAIL, magic links, sessions, and then lets authenticated users register passkeys and create API keys.

Spec: docs/web-specs/03-admin-auth-keys-passkeys.md

Requirements

  • Add environment-driven owner allow-list with ADMIN_EMAIL.
  • Add SQLite migrations for magic links and admin sessions.
  • Store only hashed magic-link tokens; never persist raw tokens.
  • Generate magic-link tokens with Node built-in cryptographic randomness.
  • Expire magic links after a short fixed window, e.g. 15 minutes.
  • Mark magic links as used after successful verification so each link is single-use.
  • Add GET /login with an email form and clear success/error messages.
  • Add POST /login to request a magic link.
  • Avoid leaking whether an email is authorized; unauthorized requests should render the same generic success message but create no usable link.
  • In development mode, log the full magic link to server logs instead of requiring SMTP.
  • Leave production email delivery behind a small service boundary; if SMTP is not implemented in this slice, fail closed with a clear operational error rather than silently pretending to send.
  • Add GET /login/verify?token=... to verify the token and create a server-side session.
  • Store session IDs server-side and set an HttpOnly, SameSite=Lax session cookie.
  • Add POST /logout and optionally GET /logout to delete the server-side session and clear the cookie.
  • Add auth middleware for admin web routes.
  • Add a minimal protected GET /admin dashboard proving the session works.
  • Do not implement API key management UI, passkey registration, or passkey login in this slice.

Acceptance criteria

  • GET /login renders an email login form.
  • Authorized POST /login creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or the configured delivery boundary.
  • Unauthorized POST /login does not create a usable magic link and does not reveal authorization status to the requester.
  • GET /login/verify with a valid token creates a server-side session and sets a secure cookie.
  • Reusing, expiring, malformed, or unknown magic-link tokens does not create a session.
  • GET /admin redirects anonymous users to /login.
  • GET /admin succeeds for authenticated owner sessions.
  • Logout deletes the server-side session and clears the browser cookie.
  • Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout.
  • Relevant lint/test checks pass.

Dependencies

  • task-090a180b
## Goal Implement the initial admin authentication bootstrap used before passkeys exist: an `ADMIN_EMAIL` allow-list, magic-link login, session cookies, logout, and a protected admin dashboard. This is the first trusted login path; passkeys are registered only after this session exists. Reference behavior: `/home/erik/Private/code/github/evcraddock/erikcraddock.me` uses `ADMIN_EMAIL`, magic links, sessions, and then lets authenticated users register passkeys and create API keys. Spec: `docs/web-specs/03-admin-auth-keys-passkeys.md` ## Requirements - Add environment-driven owner allow-list with `ADMIN_EMAIL`. - Add SQLite migrations for magic links and admin sessions. - Store only hashed magic-link tokens; never persist raw tokens. - Generate magic-link tokens with Node built-in cryptographic randomness. - Expire magic links after a short fixed window, e.g. 15 minutes. - Mark magic links as used after successful verification so each link is single-use. - Add `GET /login` with an email form and clear success/error messages. - Add `POST /login` to request a magic link. - Avoid leaking whether an email is authorized; unauthorized requests should render the same generic success message but create no usable link. - In development mode, log the full magic link to server logs instead of requiring SMTP. - Leave production email delivery behind a small service boundary; if SMTP is not implemented in this slice, fail closed with a clear operational error rather than silently pretending to send. - Add `GET /login/verify?token=...` to verify the token and create a server-side session. - Store session IDs server-side and set an `HttpOnly`, `SameSite=Lax` session cookie. - Add `POST /logout` and optionally `GET /logout` to delete the server-side session and clear the cookie. - Add auth middleware for admin web routes. - Add a minimal protected `GET /admin` dashboard proving the session works. - Do not implement API key management UI, passkey registration, or passkey login in this slice. ## Acceptance criteria - [ ] `GET /login` renders an email login form. - [ ] Authorized `POST /login` creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or the configured delivery boundary. - [ ] Unauthorized `POST /login` does not create a usable magic link and does not reveal authorization status to the requester. - [ ] `GET /login/verify` with a valid token creates a server-side session and sets a secure cookie. - [ ] Reusing, expiring, malformed, or unknown magic-link tokens does not create a session. - [ ] `GET /admin` redirects anonymous users to `/login`. - [ ] `GET /admin` succeeds for authenticated owner sessions. - [ ] Logout deletes the server-side session and clears the browser cookie. - [ ] Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout. - [ ] Relevant lint/test checks pass. ## Dependencies - task-090a180b
erik changed title from Add admin login and session routes to Add admin magic-link login and sessions 2026-05-22 12:57:01 -05:00
Author
Owner

Synced from todu comment by @todu on 2026-05-22T17:54:58.255Z

Updated this task to follow the proven erikcraddock.me bootstrap model:

  • ADMIN_EMAIL is the initial trust anchor.
  • /login requests a magic link.
  • magic-link tokens are hashed in DB, short-lived, and single-use.
  • dev mode logs the link; production delivery is behind an explicit service boundary.
  • /login/verify creates a server-side session cookie.
  • /admin is protected by that session.
  • passkeys are registered only after this authenticated session exists.

Also updated follow-up tasks so passkey registration/login and API key UI depend on this session foundation, and created task-b8374859 for the later slug login browser flow.

_Synced from todu comment by @todu on 2026-05-22T17:54:58.255Z_ ## Scope Updated: Magic-Link Bootstrap Updated this task to follow the proven `erikcraddock.me` bootstrap model: - `ADMIN_EMAIL` is the initial trust anchor. - `/login` requests a magic link. - magic-link tokens are hashed in DB, short-lived, and single-use. - dev mode logs the link; production delivery is behind an explicit service boundary. - `/login/verify` creates a server-side session cookie. - `/admin` is protected by that session. - passkeys are registered only after this authenticated session exists. Also updated follow-up tasks so passkey registration/login and API key UI depend on this session foundation, and created `task-b8374859` for the later `slug login` browser flow.
Author
Owner

Synced from todu comment by @todu on 2026-05-22T18:05:22.713Z

PR Review: Approved

PR: #58

Summary

Reviewed PR #58 at commit 0b75a6a. The PR implements the magic-link bootstrap slice: ADMIN_EMAIL is the owner allow-list, magic-link tokens are hashed, expiring, and single-use, development delivery logs the link, production delivery fails closed when not configured, server-side sessions and cookies are added, and /login, /login/verify, /logout, and protected /admin routes are wired with tests.

Acceptance Criteria

  • GET /login renders an email login form.
  • Authorized POST /login creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or the configured delivery boundary.
  • Unauthorized POST /login does not create a usable magic link and does not reveal authorization status to the requester.
  • GET /login/verify with a valid token creates a server-side session and sets a secure cookie.
  • Reusing, expiring, malformed, or unknown magic-link tokens does not create a session.
  • GET /admin redirects anonymous users to /login.
  • GET /admin succeeds for authenticated owner sessions.
  • Logout deletes the server-side session and clears the browser cookie.
  • Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout.
  • Relevant lint/test checks pass — make check, ./scripts/pre-pr.sh, manual smoke, and Forgejo CI passed.

Blocking Issues

None.

Warnings

None.

Verdict

Approved for merge.

_Synced from todu comment by @todu on 2026-05-22T18:05:22.713Z_ ## PR Review: Approved PR: https://forge.caradoc.com/erik/slugkit/pulls/58 ### Summary Reviewed PR #58 at commit `0b75a6a`. The PR implements the magic-link bootstrap slice: `ADMIN_EMAIL` is the owner allow-list, magic-link tokens are hashed, expiring, and single-use, development delivery logs the link, production delivery fails closed when not configured, server-side sessions and cookies are added, and `/login`, `/login/verify`, `/logout`, and protected `/admin` routes are wired with tests. ### Acceptance Criteria - [x] `GET /login` renders an email login form. - [x] Authorized `POST /login` creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or the configured delivery boundary. - [x] Unauthorized `POST /login` does not create a usable magic link and does not reveal authorization status to the requester. - [x] `GET /login/verify` with a valid token creates a server-side session and sets a secure cookie. - [x] Reusing, expiring, malformed, or unknown magic-link tokens does not create a session. - [x] `GET /admin` redirects anonymous users to `/login`. - [x] `GET /admin` succeeds for authenticated owner sessions. - [x] Logout deletes the server-side session and clears the browser cookie. - [x] Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout. - [x] Relevant lint/test checks pass — `make check`, `./scripts/pre-pr.sh`, manual smoke, and Forgejo CI passed. ### Blocking Issues None. ### Warnings None. ### Verdict Approved for merge.
erik 2026-05-22 13:54:55 -05:00
Author
Owner

Synced from todu comment by @todu on 2026-05-22T18:53:40.560Z

Closing Summary

PR #58 merged: #58

Acceptance criteria evidence:

  • GET /login renders an email login form: met.
  • Authorized POST /login creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or delivery boundary: met.
  • Unauthorized POST /login does not create a usable magic link and does not reveal authorization status: met.
  • GET /login/verify with a valid token creates a server-side session and sets an HttpOnly, SameSite=Lax cookie: met.
  • Reusing, expiring, malformed, or unknown magic-link tokens does not create a session: met.
  • GET /admin redirects anonymous users to /login: met.
  • GET /admin succeeds for authenticated owner sessions: met.
  • Logout deletes the server-side session and clears the browser cookie: met.
  • Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout: met.
  • Relevant lint/test checks pass: met via make check, ./scripts/pre-pr.sh, PR CI, and main push CI.

Additional shipped behavior:

  • Startup applies pending migrations before serving.
  • Default admin email is admin@slugkit.com when unset.
  • Dev startup logs show the effective admin email.
  • Auth lifecycle logs report magic-link creation, verification, session creation, login success, and logout without raw tokens or session IDs.
  • Runtime data/ paths are ignored.

Post-merge cleanup: local and remote feature branches deleted.

Readiness: READY

_Synced from todu comment by @todu on 2026-05-22T18:53:40.560Z_ ## Closing Summary PR #58 merged: https://forge.caradoc.com/erik/slugkit/pulls/58 Acceptance criteria evidence: - `GET /login` renders an email login form: met. - Authorized `POST /login` creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or delivery boundary: met. - Unauthorized `POST /login` does not create a usable magic link and does not reveal authorization status: met. - `GET /login/verify` with a valid token creates a server-side session and sets an `HttpOnly`, `SameSite=Lax` cookie: met. - Reusing, expiring, malformed, or unknown magic-link tokens does not create a session: met. - `GET /admin` redirects anonymous users to `/login`: met. - `GET /admin` succeeds for authenticated owner sessions: met. - Logout deletes the server-side session and clears the browser cookie: met. - Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout: met. - Relevant lint/test checks pass: met via `make check`, `./scripts/pre-pr.sh`, PR CI, and main push CI. Additional shipped behavior: - Startup applies pending migrations before serving. - Default admin email is `admin@slugkit.com` when unset. - Dev startup logs show the effective admin email. - Auth lifecycle logs report magic-link creation, verification, session creation, login success, and logout without raw tokens or session IDs. - Runtime `data/` paths are ignored. Post-merge cleanup: local and remote feature branches deleted. Readiness: READY
Author
Owner

Synced from todu comment by @todu on 2026-05-22T18:50:00.036Z

PR Review: Approved (updated)

PR: #58

Summary

Reviewed updated PR #58 through commit ec095f4. The PR now includes the original magic-link/session bootstrap plus the follow-up fixes found during manual testing: startup migrations run before serving, default admin email falls back to admin@slugkit.com, dev startup logs show the effective admin email, auth lifecycle events are logged, and runtime SQLite data is ignored.

Acceptance Criteria

  • GET /login renders an email login form.
  • Authorized POST /login creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or the configured delivery boundary.
  • Unauthorized POST /login does not create a usable magic link and does not reveal authorization status to the requester.
  • GET /login/verify with a valid token creates a server-side session and sets a secure cookie.
  • Reusing, expiring, malformed, or unknown magic-link tokens does not create a session.
  • GET /admin redirects anonymous users to /login.
  • GET /admin succeeds for authenticated owner sessions.
  • Logout deletes the server-side session and clears the browser cookie.
  • Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout.
  • Relevant lint/test checks pass — make check, ./scripts/pre-pr.sh, manual login smoke, and Forgejo CI passed.

Additional Verification

  • Startup applies pending migrations before Hono serves requests.
  • Dev startup logs show ADMIN_EMAIL=admin@slugkit.com when unset.
  • Magic-link request and successful login now emit clear auth lifecycle logs without tokens or session IDs.
  • Runtime data/ paths are ignored.

Blocking Issues

None.

Warnings

None.

Verdict

Approved for merge.

_Synced from todu comment by @todu on 2026-05-22T18:50:00.036Z_ ## PR Review: Approved (updated) PR: https://forge.caradoc.com/erik/slugkit/pulls/58 ### Summary Reviewed updated PR #58 through commit `ec095f4`. The PR now includes the original magic-link/session bootstrap plus the follow-up fixes found during manual testing: startup migrations run before serving, default admin email falls back to `admin@slugkit.com`, dev startup logs show the effective admin email, auth lifecycle events are logged, and runtime SQLite data is ignored. ### Acceptance Criteria - [x] `GET /login` renders an email login form. - [x] Authorized `POST /login` creates a hashed, expiring, single-use magic-link record and exposes the raw link only through dev logging or the configured delivery boundary. - [x] Unauthorized `POST /login` does not create a usable magic link and does not reveal authorization status to the requester. - [x] `GET /login/verify` with a valid token creates a server-side session and sets a secure cookie. - [x] Reusing, expiring, malformed, or unknown magic-link tokens does not create a session. - [x] `GET /admin` redirects anonymous users to `/login`. - [x] `GET /admin` succeeds for authenticated owner sessions. - [x] Logout deletes the server-side session and clears the browser cookie. - [x] Tests cover authorized login request, unauthorized login request, successful verification, single-use token behavior, expired token behavior, protected admin access, and logout. - [x] Relevant lint/test checks pass — `make check`, `./scripts/pre-pr.sh`, manual login smoke, and Forgejo CI passed. ### Additional Verification - Startup applies pending migrations before Hono serves requests. - Dev startup logs show `ADMIN_EMAIL=admin@slugkit.com` when unset. - Magic-link request and successful login now emit clear auth lifecycle logs without tokens or session IDs. - Runtime `data/` paths are ignored. ### Blocking Issues None. ### Warnings None. ### Verdict Approved for merge.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
erik/slugkit#55
No description provided.