jeswr / jeswr/auth-solid

Design + decisions: @jeswr/auth-solid (Solid-OIDC provider for Auth.js) — maintainer steer

Open
#1 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

**Package:** `@jeswr/auth-solid` — a Solid-OIDC provider for Auth.js (next-auth v5 / `@auth/core`). Built, gate-green, roborev-clean, GitHub-installable (`github:jeswr/auth-solid#main`). This issue records the design decisions taken under the standing "don't block on greenlights — make the best call, document it, file an issue for steer" rule. Flagging the ones worth a maintainer look.

## What it is
`Solid(config)` returns an Auth.js `OIDCConfig` you drop into `NextAuth({ providers: [await Solid({ issuer, clientId })] })`. It adds the Solid-specific seams Auth.js's generic OIDC flow lacks: PKCE(S256)+state+nonce (all mandatory), scope `openid webid offline_access`, DPoP-bound (sender-constrained, RFC 9449) tokens via the Auth.js `customFetch` symbol, verified-WebID-only profile mapping, and a `solidDpopFetch` for DPoP-authed pod requests from the persisted session.

## Design decisions (made + documented — confirm or redirect)

1. **Compose `@jeswr/solid-dpop` DIRECTLY, NOT `@jeswr/solid-openid-client`.** Auth.js owns the OAuth orchestration (via oauth4webapi); `solid-openid-client` runs its OWN auth-code/callback flow (`authorizationUrl`/`handleCallback`) which would fight Auth.js's. We only need to inject DPoP proofs at the HTTP layer — exactly what `solid-dpop`'s proof primitives give us. So `solid-dpop` is a normal dep (bundled into `dist/`); `solid-openid-client` is not used here.

2. **DPoP via the `customFetch` seam, because `@auth/core` does NOT do DPoP itself** (verified against the installed source — it builds a plain token request). Auth.js routes ALL OAuth-endpoint HTTP (discovery, JWKS, token, userinfo) through one `customFetch`; ours **discriminates** and attaches a proof ONLY to the token-endpoint leg (POST + `application/x-www-form-urlencoded`). Token-leg proof carries NO `ath` (RFC 9449 §4.2); the §8 `use_dpop_nonce` retry is done exactly once. Discovery/JWKS/userinfo pass through untouched.

3. **DPoP key persistence shape = into the Auth.js JWT (default), with a loud tradeoff.** On first sign-in the documented `jwt` callback persists the DPoP-bound `access_token`/`refresh_token`/`id_token`/`expires_at` AND the serialized DPoP PRIVATE JWK into the token (the refresh-token `jkt` binding REQUIRES the same key after a restart). This puts a private key + tokens into the JWT — so we REQUIRE an encrypted JWT session (Auth.js encrypts the JWT with `AUTH_SECRET` by default) and document it prominently. **Steer wanted:** is JWT-session the right default, or would you prefer the README lead with a database-session example (same fields in the account/session row) for apps that don't want key material in a cookie? Both are documented; JWT is currently the headline.

4. **Verified-WebID-only, fail-closed.** The WebID is read ONLY from the verified ID-token claims Auth.js passes to `profile()` (oauth4webapi has validated the ID token's signature/iss/aud/nonce). Primary the `webid` claim, fallback a `sub` that is itself an http(s) WebID. No `webid` -> throw (no session without a verified WebID). Never trusts the access token.

5. **Provider factory is ASYNC** (`Solid(...)` returns a Promise) because it must prepare the ES256 DPoP keypair before `customFetch` can sign. Consumers `await` it once at module load. Acceptable for Auth.js v5 (config is built once), but flagging it as a slightly unusual provider shape.

6. **`@auth/core >=0.37` peer floor (important install gotcha).** The `customFetch` named export landed in `@auth/core` 0.37.0, and npm's `latest` dist-tag currently LAGS at 0.34.3 (no such export) — a bare `npm install @auth/core` pulls an export-less version that breaks the import. Peer floor is `>=0.37.0 <1`; README tells consumers to pin `@auth/core@^0.37` or use `next-auth@5`. (Regression-tested.)

## Security posture
PKCE(S256)+state+nonce always; DPoP mandatory + asymmetric-only (ES256 via solid-dpop, no symmetric/`none` path reachable); verified-WebID-only fail-closed; DPoP-downgrade guard (a non-DPoP `token_type` fails closed); transport guards (https issuer/endpoints unless `allowInsecure` on loopback — a token/proof never goes over plaintext); §8 nonce retry capped at 1 (no loop); single-use proofs (fresh `jti`); no token/proof/key ever logged; bounded+cancellable replay buffer for stream bodies. Tests run against a faithful Map-backed mock OP that verifies inbound DPoP proofs for real (signature, `typ`, `htm`/`htu`, ES256-asymmetric, fresh `jti`) and exercises the §8 nonce flow — NO live IdP. 73 tests, roborev-clean.

## needs:user / follow-ups (NOT blockers)
- **npm publish** (deferred — needs npm login). GitHub-install works now.
- **Live-IdP E2E** against a real Solid OP (CSS / Keycloak) to complement the mock-OP unit suite.
- **solid-ai-coding lesson candidate:** the "Auth.js does not do DPoP; inject it via the `customFetch` seam (token-leg-only, no `ath`, §8 retry)" pattern + the DPoP-key-in-JWT tradeoff are genuinely portable — worth a skill note if you want Auth.js coverage documented in the suite skills.

---
🤖 PSS agent — @jeswr's agent for `prod-solid-server` / the Solid app+Pod-Manager suite

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the Solid(config) provider factory and its Auth.js customFetch integration, then review the README, installed @auth/core source, and the 73-test Map-backed mock-OP suite. Done requires a maintainer decision on the flagged JWT-session tradeoff and provider shape, with any resulting work separated into a concrete follow-up issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.