Skip to content

feat: browser sign-in for the UI (Microsoft, administrator password, access token) - #17

Merged
hujincalrin41 merged 2 commits into
mainfrom
feature/ui-sign-in
Sep 23, 2026
Merged

hujincalrin41 merged 2 commits into
mainfrom
feature/ui-sign-in

Conversation

@hujincalrin41

Copy link
Copy Markdown
Contributor

What

The UI had no way to sign anyone in. The UI server put an administrator token on every API call it forwarded, so opening its address was enough to be admin, and there was no login page to speak of.

This gives each way in an address of its own:

Address Who Enabled by
/login — the default page, Sign in with Microsoft employees, with their M365 account server.authorization-url … server.client-secret
/login/admin — a password form, not linked from the login page whoever runs the server server.admin-password
/login/token — paste an access token this server issued whoever runs the server always available

The application is no longer an entry point: without a session it redirects to /login. The UI server adds no credentials of its own. Which ways exist is asked of the server at runtime (GET /auth/providers), so one UI build follows any deployment's configuration.

Microsoft sign-in runs through the server: it redirects to the provider, redeems the code itself and sets the session cookie, so the client secret never reaches the browser.

Server side

Accepting an identity provider's tokens needed more than an entry point:

  • The static JWKS file and OIDC discovery now coexist. They were mutually exclusive: a JWKS file, once configured, was used for every issuer, so a deployment could trust its own instances or an identity provider, never both. Routing is by whether the issuer is registered in the file.
  • The caller resolves through an ordered claim chain — email, preferred_username, upn, sub — because Entra ID omits email unless it is configured as an optional claim. All four are looked up as the user's email; the authorization model is untouched.
  • Discovery is cached per issuer and every call to the provider is bounded. An unreachable provider answers 401 naming the issuer instead of a bodyless 500.
  • Entra's {tenantid} issuer template is compared segment-wise, which multi-tenant endpoints advertise literally.
  • The session cookie is one a browser keeps: Secure only when the browser is on HTTPS, and SameSite=Lax rather than Strict — the browser returns from the provider on a cross-site navigation, exactly the request a Strict cookie is withheld on.
  • The URL transcoder passes a redirect back instead of following it, and ends a bodyless response on its headers. It followed the sign-in redirect itself and relayed the provider's error page as a 400.
  • A refused authorization code names the provider's error code (invalid_client, AADSTS7000222), which is what separates an expired secret from a mismatched redirect URI or a replayed code; the full description is logged at warn.

Deployment

deploy-uc.sh derives what the token check needs from the four Microsoft settings — the audience always, the issuer when the authorization URL states it unambiguously — and prints what it added. It starts the UI alongside the server, and warns before rendering secrets over a file that git tracks. Both deployment guides document the Entra setup, including what the client secret's expiry breaks and how to rotate it without losing a sign-in; the Chinese guide had no sign-in chapter at all.

Tests

Server 83 (dual trust chains, identity resolution, issuer templates, discovery caching, hosted login, administrator sign-in configured and not, token sign-in, providers), UI 82 across 22 suites including the route table. javafmtCheckAll, yarn test:format and yarn build are clean.

Verified against a real Entra tenant end to end: the redirect carries the registered client id and callback, and a signed-in user's catalog and table lists are filtered by their grants.

Note for review

This touches JwksOperations and AuthService, which #15 also changes. Whichever lands second will need a rebase there; the rest of this change does not overlap.

…access token)

The UI had no way to sign anyone in: the UI server put an administrator token on
every API call it forwarded, so opening its address was enough to be admin, and
there was no login page to speak of.

Give each way in an address of its own. /login is the default page and carries
Sign in with Microsoft, through an OAuth flow the server hosts end to end, so the
client secret never reaches the browser. /login/admin takes the password from
server.admin-password, and /login/token takes an access token this server issued,
for whoever runs it. None is linked from another, the application itself is no
longer an entry point -- without a session it redirects to /login -- and the UI
server adds no credentials of its own. Which ways exist is asked of the server at
runtime, so one UI build follows any deployment's configuration.

Accepting an identity provider's tokens needed the server side to change too:

- the static JWKS file and OIDC discovery now coexist, routed by whether the
  issuer is registered in the file, so a Relyt instance and Entra ID are both
  trusted at once
- the caller resolves through an ordered claim chain (email, preferred_username,
  upn, sub), because Entra omits email unless it is configured as an optional claim
- discovered providers are cached per issuer and every call to the provider is
  bounded; an unreachable one answers 401 with its name rather than a bodyless 500
- Entra's {tenantid} issuer template is compared segment-wise, which multi-tenant
  endpoints advertise literally
- the session cookie is one a browser keeps: Secure only when the browser is on
  HTTPS, read the way the callback URL is read, and SameSite=Lax, since the browser
  returns from the provider on a cross-site navigation
- the URL transcoder passes a redirect back instead of following it, and ends a
  bodyless response on its headers

Deployment follows: deploy-uc.sh derives the audience and, where the authorization
URL states it unambiguously, the issuer; it starts the UI alongside the server, and
warns when it is about to render secrets over a file that git tracks. The Entra
setup, including what the client secret's expiry does and how to rotate it without
losing a sign-in, is documented in both deployment guides.
The known-gap note it removes is what this PR closes.
@hujincalrin41
hujincalrin41 merged commit 88e6c42 into main Sep 23, 2026
13 checks passed
@hujincalrin41
hujincalrin41 deleted the feature/ui-sign-in branch September 23, 2026 05:30
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.

2 participants