1927 research ory kratos setup for guest users - #1928
Draft
MrDirkelz wants to merge 15 commits into
Draft
Conversation
MrDirkelz
force-pushed
the
1927-research-ory-kratos-setup-for-guest-users
branch
2 times, most recently
from
August 25, 2026 14:12
c8b400f to
f9abde6
Compare
Twenty-one screens covering the guest path, email one-time-code login, registration and verification, recovery, account settings and the error states — built as real components rather than pictures of them, so the wiring pass replaces prop values with flow data instead of markup. A dev-only canvas at /design/auth renders every screen and state in both themes side by side. It ships in no build. authCopy.ts holds the English default for each string keyed by the i18n key it will use, and useAuthCopy() prefers a translation when the language docs carry one, so the screens read correctly before any key exists. docs/temp_kratos-guest-auth-ui.md records what "guest" means here (no Kratos session — the API already serves anonymous callers), why the pages belong in the PWA rather than a standalone self-service UI, and the screen-to-flow mapping.
Sign-up and sign-in both run on the emailed one-time code, through the screens designed in the previous commit. One page serves login, signup, verification and recovery, because Kratos models them as the same two-step shape: address, then code. The payload is built from the flow's own ui.nodes rather than hand- written, which is what keeps csrf_token, the traits Kratos echoes back, and any field a later Kratos version adds in the body without a change here. Every submit outcome is a value, not a throw — a 400 carrying validation errors is an ordinary step in these flows. kratos/ holds a docker-compose Kratos and Mailpit for development. Kratos' serve.public.base_url is the app's own origin plus /.ory, which Vite proxies to the container, so every URL Kratos hands the browser is same-site and its cookies survive. Production wants the same shape from a real reverse proxy. Registered only when VITE_KRATOS_URL is set, so the routes exist nowhere they have not been switched on. The specs assert against flows recorded from Kratos v1.3.1 rather than against the documented shapes. What that recording settled: method=code sends the registration code directly without the two-step profile screen, login asks for identifier where registration asks for traits.email, and flat dotted trait keys are accepted as JSON. A Kratos session still does not authenticate anything to the Luminary API, which validates JWTs against a provider's JWKS. Closing that is a deliberate second step, written up in the doc.
/auth/account was routed at the settings flow, which renders as an address prompt — the settings flow has none of the two-step shape the other screens are built around. It reads the session directly instead, so it is a page of its own rather than another flowType. Kratos leaves the current session out of GET /sessions by design; it comes from whoami, which carries the same devices and authenticated_at fields. The screen joins the two and puts this device first, or the "this device" row never appears. Identity deletion is not part of Kratos' self-service API, so the screen takes a canDelete prop and the PoC hides the offer rather than showing one that cannot work. The route-count spec now asserts that dev-only routes are flagged rather than counting the whole table, which depended on whether the PoC happened to be switched on in the environment running the tests.
The screens existed but nothing led to them. The provider modal now offers "Continue with email" beside the configured providers, which routes to /auth/login carrying the current path as return_to rather than going through loginWithProvider — Kratos is not an OIDC provider, and an AuthProvider doc pointed at it would redirect to an /authorize endpoint that does not exist. The entry counts as a sign-in method, so the "no methods available" state no longer shows when it is the only one. A Kratos session is held separately from the OIDC one and surfaced as its own profile-menu entry linking to /auth/account. Conflating them would have the app claim an authority it does not have: the Luminary API honours the OIDC token and knows nothing about a Kratos session. All of it is behind VITE_KRATOS_URL, so an environment without the proof of concept sees exactly what it saw before. The modal's specs now drive that flag explicitly instead of inheriting whatever the developer's own .env happens to say.
Kratos answers 400 session_already_available rather than opening a login or registration flow while a session is live. createFlow threw on any non-OK status, so clicking the guest entry with a session already in the browser produced "Something went wrong — the sign-in service could not be reached", which is neither what happened nor something the user can act on. Starting a flow now reports its three outcomes as values, and an existing session sends the user to their account instead. A failure to reach Kratos still reports as one, so the two stay distinguishable. Also renames the entry to "Sign in as Guest", in the provider modal and on the method chooser both.
Completing a guest sign-in left "Login" in the menu. Two causes, both mine: the flow completes without a page load, so the shared session ref was never re-read; and the profile menu and desktop sidebar render their auth row from hard-coded markup rather than from the navigation list the earlier commit changed, so only the dropdown ever saw a guest. The auth row now has three states rather than two — signed in, guest, or neither — and the account name falls back to the guest identity in both surfaces. Signing out of a guest session is offered where signing out of an OIDC session would be. The label reads a possibly-plain `user` through unref: it is a ref in the app and a bare object in several mounted tests, and the template's unwrapping used to hide the difference.
Kratos cannot be an AuthProvider — no /authorize, no client_id, no JWKS, no JWT. Hydra can. Kratos authenticates and Hydra issues the token, so a guest is now an ordinary provider row: the app's oidc-client-ts redirect and the API's JWKS validation both apply with no Kratos special case anywhere, and the CMS manages it like any other provider. Gone with it is the parallel guest session the Kratos-only PoC needed — its shared ref, its entry in the provider modal, and the guest branches in the profile menu and sidebar. The OIDC session is the session again; a second one beside it was a second answer to who is signed in. api/src/oauth/ handles Hydra's consent and logout hand-offs. Accepting either needs Hydra's admin API, which a browser must never reach, so urls.consent points at the API rather than the web client. It grants only what was asked for and only for the one trusted client; any other client is refused rather than quietly approved. providerBaseUrl() honours an explicit scheme on a provider's domain and still assumes https without one — the rule app/src/auth.ts already applied on the client. The issuer and JWKS URL were hard-coded to https, so no provider could be reached over http and no local Hydra could be validated at all. Two things the running stack settled, neither of them what the docs imply. skip_consent on the client does not remove the consent round trip: Hydra still redirects and still expects an accept. And a browser flow must be navigated to rather than fetched — asking Kratos for one with Accept: application/json returns 200 null in exactly the case where it could have completed the flow itself. Verified end to end against Hydra v2.2.0: iss, aud, client_id and sub land where the validator expects them, tier arrives promoted out of ext, and a refresh token is issued, which signinSilent() requires.
- Kratos handles identity/password auth; Hydra issues OAuth2 tokens consumed by Luminary's API, since the API validates bearer access tokens rather than Kratos sessions directly - Adds a minimal login/consent bridge server to wire Kratos flows into Hydra's login/consent challenges, plus Caddy for local TLS routing - Includes docker-compose.yml.bak as a reference of the pre-Hydra, Kratos-only setup this replaces
- Provides a browser-based dashboard (admin.luminary.local) to list/search identities and OAuth clients, revoke sessions across both Kratos and Hydra, and delete identities/clients during local PoC development. - Wires the new `admin` service into docker-compose and Caddy alongside the existing login-consent flow.
The markup moves to login-consent/views.js as plain functions, so the pages can be rendered — and looked at — without Kratos, Hydra or a browser session. Visually they follow the app: the wordmark from logo.svg with the text on currentColor so it survives dark mode, the card treatment and type scale from the app's own components, and the amber primary button. Light and dark both, from prefers-color-scheme. Three things were not cosmetic. Error text was interpolated into the page unescaped, and it comes from Kratos' flow messages, which can carry what a user typed. There was no viewport meta, so the pages rendered desktop-width on a phone. And the 500 handler echoed err.message to the browser — an exception from Kratos or Hydra says more about the deployment than a user needs, so it goes to the log and the browser gets a page instead. Also: labels tied to their inputs, autocomplete hints, autofocus, and aria-invalid with role="alert" when a submission is refused.
Drop the in-repo Kratos/Hydra integration (api OAuth controller, app auth screens and routes, the ory/ compose stack, and the design notes) and keep only ory-kratos-setup-poc, which stands on its own and does not touch the application. Also drop the superseded docker-compose.yml.bak left in the PoC directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkEPfBPbGbJUmYDLkunTWD
A second standalone Compose stack aimed at the same target as ory-kratos-setup-poc: an OIDC provider the API accepts as an ordinary AuthProvider doc, with no change to api/ or app/. Zitadel bundles the OIDC provider, user management, login UI and admin console in one service, so the stack is three long-running containers instead of seven and needs none of the login/consent or admin code the Ory stack requires. verify-contract.mjs re-implements the checks authIdentity.service.ts performs and reports each against the running stack, so the comparison rests on observed behaviour rather than on documentation. Two constraints are predicted to fail — the JWKS path, worked around in the Caddyfile, and the issuer's trailing slash, which the README writes up without changing the API. The stack has not been run against a Docker daemon yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkEPfBPbGbJUmYDLkunTWD
…nd improve branding
MrDirkelz
force-pushed
the
1927-research-ory-kratos-setup-for-guest-users
branch
from
August 26, 2026 09:19
d129d4d to
cacc724
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.