Skip to content

Extract an AuthProvider interface behind Wasp's session layer - #4714

Closed
FranjoMindek wants to merge 4 commits into
mainfrom
franjo/auth-provider-interface
Closed

Extract an AuthProvider interface behind Wasp's session layer#4714
FranjoMindek wants to merge 4 commits into
mainfrom
franjo/auth-provider-interface

Conversation

@FranjoMindek

Copy link
Copy Markdown
Contributor

Description

Stack: PR 1 of a series exploring a pluggable auth provider interface for Wasp. This one is a pure internal refactor with no user-visible change.

Wasp's session layer imports Lucia directly and leaks Lucia's Session type out of session.ts. This PR puts an explicit AuthProvider interface in between, implemented by today's Lucia-backed code, so that the request middleware, the websocket handler, the logout route and the login paths depend on an interface rather than on a session library.

Lucia is now confined to exactly two files (lucia.ts and provider/wasp.ts). Nothing else references it.

This is valuable on its own — it is essentially what #3081 and #4677 ask for — and it is a prerequisite for making the provider selectable later.

Why this shape

I designed this four different ways (minimalist / capability-tiers / inverted-control / migration-first) and had them judged against concrete criteria. The decisions that survived:

Verify, not fetch. The primitive is verifyRequest(req) -> VerifiedSession | null, not findById(id). AdonisJS shapes its provider contract around findById, which assumes identity is retrievable by an opaque id — exactly the assumption an external provider breaks, since it validates a signed credential and cannot look a subject up on our behalf. Rejected findById for that reason.

Base interface + capability extension, not one interface with optional members. RedwoodJS modelled every optional capability as a separate generic and ended up with 13 type parameters on its client interface. SessionIssuingAuthProvider extends AuthProvider keeps the base readable. There are exactly two tiers today, so two interfaces is the right size; if it grows past ~3 the better encoding is a single AuthProvider<C extends Capability> with the optional half derived by Pick.

The subject stays opaque. VerifiedSession.subjectId is provider-owned and Wasp resolves it. Wasp's sessions are keyed on Auth.id, not on the developer's User.id — a port designed around issueSession(userId) would force the wasp-auth adapter to do the Auth -> User join itself (breaking "the provider never touches the DB") or force core into an extra lookup per request. This mismatch would not bite any hosted provider, only the one adapter the whole migration is built around.

Rejected: a single authenticate(Request) entry point. It is cleaner than having both verifyRequest and verifyCredential, and it is where this should end up — Clerk and WorkOS both take a Request. But websockets carry a bare token in socket.handshake.auth.sessionId, so collapsing the two means a websocket wire change. That belongs in its own PR with a back-compat window, not here.

One subtlety worth reviewing

The old code got Auth.userId for free out of Lucia's validateSession (via getUserAttributes) and then loaded the user by that id. Resolving through the provider interface loses that free ride, so a naive port would add a DB round trip per authenticated request. Instead toSessionAndUser loads the user through the auth relation in a single query:

prisma.user.findFirst({
  where: { auth: { id: authId } },
  include: { auth: { include: { identities: true } } },
})

Same query count as before, and it is the shape every future provider needs anyway.

Known signature change

wasp/server/auth/session is a public entrypoint, and getSessionAndUserFrom* now returns { sessionId, user } rather than { session, user }. Everything in that module is marked PRIVATE API and only .id was ever read from the old session object, but it is a change to an exported signature.

Not in this PR

Provider selection, JIT provisioning, capability-gated codegen, and any second adapter. provider/index.ts exports a single hardcoded authProvider — that is the seam those later PRs widen.

The full design, the uniform/tiered/provider-specific classification of Wasp's public auth API, and the rest of the stack are in waspc/docs/auth-provider-interface-plan.md, added here.

Type of change

  • 🔧 Just code/docs improvement
  • 🐞 Bug fix
  • 🚀 New/improved feature
  • 💥 Breaking change

Checklist

  • I tested my change in a Wasp app to verify that it works as intended.

    Verified live against a running app — signup 200, login returns sessionId, /auth/me 200 with the full AuthUser and identities intact, wrong password 401, logout 200, /auth/me 401 after logout, no-token {"json":null}. Identical to pre-refactor behaviour.

  • 🧪 Tests and apps:

    • I added unit tests for my change.

      No new unit tests: this is a behaviour-preserving refactor and the existing e2e golden snapshots are the stronger check — they compare generated output verbatim. All 932 e2e tests pass.

    • (if you fixed a bug) I added a regression test for the bug I fixed.

    • (if you added/updated a feature) I added/updated e2e tests in examples/kitchen-sink/e2e-tests.

    • (if you added/updated a feature) I updated the starter templates in waspc/data/Cli/templates, as needed.

    • (if you added/updated a feature) I updated the example apps in examples/, as needed.

      • (if you updated examples/tutorials) I updated the tutorial in the docs (and vice versa).
  • 📜 Documentation:

    • (if you added/updated a feature) I added/updated the documentation in web/docs/.

      No user-facing change to document. Design rationale is in waspc/docs/.

  • 🆕 Changelog: (if change is more than just code/docs improvement)

    • I updated waspc/ChangeLog.md with a user-friendly description of the change.
    • (if you did a breaking change) I added a step to the current migration guide in web/docs/migration-guides/.
    • I bumped the version in waspc/waspc.cabal to reflect the changes I introduced.

Note on the goldens

I excluded one file the regeneration touched: wasp-build-golden/.../web-app/build/assets/200.js. It is a bundler formatting change (braces added around an if/else) in client output this PR does not touch, and it reproduced through a clean accept-all, so it looks like local toolchain drift rather than anything here. Flagging it in case it is real.

@pkg-pr-new

pkg-pr-new Bot commented Aug 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@wasp.sh/spec

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/spec@4714

@wasp.sh/wasp-cli

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli@4714

@wasp.sh/wasp-cli-darwin-arm64-unknown

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-darwin-arm64-unknown@4714

@wasp.sh/wasp-cli-darwin-x64-unknown

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-darwin-x64-unknown@4714

@wasp.sh/wasp-cli-linux-arm64-glibc

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-arm64-glibc@4714

@wasp.sh/wasp-cli-linux-x64-glibc

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-x64-glibc@4714

@wasp.sh/wasp-cli-linux-x64-musl

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-x64-musl@4714

commit: 946d141

@FranjoMindek

Copy link
Copy Markdown
Contributor Author

Superseded by the restacked series (stack of #4724#4730). The AuthProvider extraction now lives in #4725, where the contract is born directly in @wasp.sh/auth-contract with its final authenticate() shape, so the intermediate verifyRequest/verifyCredential design from this PR no longer exists anywhere in the history.

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