Skip to content

Require login with Entra ID or a password for all of Saman - #23

Merged
RonnyB71 merged 3 commits into
mainfrom
feat/entra-id-login
Nov 1, 2026
Merged

RonnyB71 merged 3 commits into
mainfrom
feat/entra-id-login

Conversation

@RonnyB71

@RonnyB71 RonnyB71 commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Implements login for the first version, as decided in #21: nothing in Saman is available without logging in.

Requirements: NF-11, NF-02, NF-12, and groundwork for NF-13.

How it works

  • Payload owns the session. Both login methods end with the same payload-token cookie. Access control stays in the collection access functions.
  • Astro middleware (src/web/src/middleware.ts) asks Payload's /api/brukere/me who the cookie belongs to on every request.
    • Without a user, it redirects to /logg-inn?redirect=….
    • If the CMS can't be reached, it returns 503, so nobody gets in.
  • Entra ID (src/cms/src/auth/) uses openid-client with the authorization code flow and PKCE.
    • State, nonce and the PKCE verifier are kept in a signed cookie that lasts 10 minutes.
    • tid must match ENTRA_TENANT_ID.
    • Users are found by oid, created on first login, and linked by email to an existing user.
    • First and last name come from the optional claims given_name and family_name, which must be turned on in the app registration. A name the token doesn't carry is left unchanged.
    • After login, users are redirected only to the CMS or the web frontend.
    • Without the ENTRA_* variables the provider is off, and only email and password are offered.
  • Email and password (NF-12) remain available on /logg-inn and in the admin panel. Users created by Entra ID get a random password, so the fallback works only for users an administrator has given a password.
  • Ready for Ansattporten (NF-13): users have an identities array of { provider, subject }, and providers are a list. Ansattporten becomes one more entry once open question 8 is answered.

Changes to access

Now that everyone in Digdir can log in, the collections that let any logged-in user write had to be tightened:

  • Only administrators (roles: ['admin']) can use the admin panel, read other users, or create, update and delete users and media.
  • The first user in an empty database becomes an administrator.
  • pnpm make-admin <email> promotes an existing user. Existing dev databases: users from before this PR have no role, so run it once for yourself.

Other changes

  • Session length: sessions last 8 hours instead of Payload's 2.
  • Cookie forwarding: Payload ignores its session cookie on server-to-server requests, because they carry no Origin or Sec-Fetch-Site header for its CSRF check. So the new authHeaders(cookie) in @saman/shared sends the token as Authorization: JWT …. CLAUDE.md now says pages read as the visitor.
  • CMS front page: the template page on the CMS's / now redirects to the web frontend. The e2e readiness check waits on /api/health instead.

Tests

  • pnpm lint passes.
  • pnpm test: CMS 26 passed, web 14 passed.
  • pnpm test:e2e: 9 passed.
  • New tests:
    • CMS integration: the full Entra ID flow against oauth2-mock-server, covering first login, finding the user by oid after an email change, linking by email, another tenant, a tampered state cookie, the redirect allow-list, and the provider being off. Also access tests for users and media.
    • Web: the middleware and the login page.
    • e2e: no page without logging in, and password login and logout through the browser.
  • Manual check against the real tenant: discovery works, and Entra ID accepts the redirect URI and shows its sign-in form. A full Entra ID sign-in needs a person and hasn't been done.

Setup for a local Entra ID login

Set ENTRA_TENANT_ID, ENTRA_CLIENT_ID and ENTRA_CLIENT_SECRET in src/cms/.env. The redirect URI is http://localhost:3000/api/brukere/oauth/entra/callback. The values are not committed.

🤖 Generated with Claude Code

RonnyB71 and others added 3 commits November 1, 2026 17:29
Nothing is available without logging in (NF-11). The Astro middleware asks
Payload who the session cookie belongs to and sends everyone else to
/logg-inn. Payload stays the only session and the only access control.

- Entra ID (NF-02): the CMS runs the OpenID Connect authorization code
  flow with PKCE and issues a normal Payload session. Users are found by
  their Entra oid, created on first login, and linked to an existing user
  with the same email. The provider list is built for Ansattporten
  (NF-13) to join later.
- Email and password (NF-12) stays as the fallback, on /logg-inn and in
  the admin panel.
- Everyone in Digdir can now log in, so only administrators use the admin
  panel and change users and media. The first user becomes one, and
  `pnpm make-admin` promotes others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first-user rule is for the form on /admin of an empty database. On a
fresh deployment, whoever happened to log in with Entra ID first would
otherwise have become an administrator. CI caught it: its database is
empty when the Entra ID tests run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Users get firstName and lastName, from the optional claims given_name
and family_name, instead of the display name, which Entra ID writes as
«Birkeli, Ronny». A name the token doesn't carry is left as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RonnyB71
RonnyB71 merged commit 8a2ad06 into main Nov 1, 2026
6 checks passed
@RonnyB71
RonnyB71 deleted the feat/entra-id-login branch November 1, 2026 17:39
RonnyB71 added a commit that referenced this pull request Nov 1, 2026
The overview described an earlier plan: the frontend getting an Entra
ID access token for the CMS, and the CMS validating it. What was built
(#23, ADR-0001) runs the sign-in in the CMS and has both apps share
Payload's session. The Mermaid source, the redrawn SVG, the
authentication steps, the parts table and the local setup now say so.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RonnyB71 added a commit that referenced this pull request Nov 1, 2026
docs/adr/ records why Saman is built the way it is, next to the
specification's own log of requirement changes. ADR-0001 writes down the
login design from #21 and #23 (NF-02, NF-11, NF-12, NF-13) as accepted.
ADR-0002 proposes limiting password login (NF-12) to named break-glass
accounts, for discussion on this pull request. CLAUDE.md says when to
write a record and that proposed ones aren't built.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant