Summary
JSS's Resource-Server token validation in src/auth/solid-oidc.js is largely correct but skips the WebID-profile / solid:oidcIssuer cross-check that the Solid-OIDC spec requires. This is what makes the protocol secure-portable rather than just-permissive.
What JSS does today (correct)
- ✅ Accepts DPoP-bound tokens from any issuer — no hardcoded IdP whitelist (
src/auth/solid-oidc.js:85–162)
- ✅ Dynamically fetches the token issuer's OIDC config + JWKS (
src/auth/solid-oidc.js:309–328)
- ✅ Verifies the DPoP proof against the request
- ✅ Validates external issuer URLs against SSRF (
src/auth/solid-oidc.js:273–277); trustedIssuers is only an SSRF skip-list, not a token-acceptance gate
What's missing (steps 4–5 of the spec)
After parsing the WebID from the token's webid / sub claim (src/auth/solid-oidc.js:130), the code jumps straight to returning it. It should instead:
- Fetch the WebID profile document over HTTP (with
Accept: text/turtle, application/ld+json)
- Parse the RDF and verify that one of the profile's
solid:oidcIssuer triples matches the token's iss claim
- Reject the token if no match
The gap is between lines 129 and 142 in src/auth/solid-oidc.js.
Why this matters
Security: Without the profile check, any IdP that JSS can reach can issue tokens for any WebID claim. A hostile IdP at evil.example could mint a token with webid: https://alice.solidcommunity.net/profile/card#me and JSS would accept it. The profile check is what binds tokens to the user's declared trust set.
Portability theatre: Counterintuitively, the missing check means cross-pod auth currently "works" — tokens from any IdP are accepted. Fixing this properly will tighten the check: profiles must declare their IdPs via solid:oidcIssuer. For the common case (the IdP is the same pod that hosts the profile) this is one extra HTTP GET; the cost is real but small.
Fix sketch
Inside the token-verify path (after the webid is extracted, before the success return):
const profileDoc = await fetch(webidUrl, {
headers: { Accept: 'text/turtle, application/ld+json' }
}).then(r => r.text())
// parse RDF, find <webidUrl> <http://www.w3.org/ns/solid/terms#oidcIssuer> ?iss
const trustedIssuers = parseOidcIssuersFromProfile(profileDoc, webidUrl)
if (!trustedIssuers.includes(token.iss)) {
throw new Error(`Issuer ${token.iss} not declared in WebID profile`)
}
Cache the profile parse (5–15 min TTL) — every request would otherwise re-fetch the profile, which is expensive at scale.
Why I'm filing
The solid-apps/explorer is now multi-pod (sidebar + split mode in PRs #6/#7). The natural follow-up was "do I need to log into both pods?" — Solid-OIDC's whole point is "no, you don't". JSS gets most of the way there; this is the last spec-completeness gap before portable cross-pod auth is genuinely safe.
Out of scope
- The
iss claim value lookup (already handled correctly)
- DPoP proof validation (already correct)
- The IdP side (
src/idp/) — this issue is RS-only
Related
Summary
JSS's Resource-Server token validation in
src/auth/solid-oidc.jsis largely correct but skips the WebID-profile /solid:oidcIssuercross-check that the Solid-OIDC spec requires. This is what makes the protocol secure-portable rather than just-permissive.What JSS does today (correct)
src/auth/solid-oidc.js:85–162)src/auth/solid-oidc.js:309–328)src/auth/solid-oidc.js:273–277);trustedIssuersis only an SSRF skip-list, not a token-acceptance gateWhat's missing (steps 4–5 of the spec)
After parsing the WebID from the token's
webid/subclaim (src/auth/solid-oidc.js:130), the code jumps straight to returning it. It should instead:Accept: text/turtle, application/ld+json)solid:oidcIssuertriples matches the token'sissclaimThe gap is between lines 129 and 142 in
src/auth/solid-oidc.js.Why this matters
Security: Without the profile check, any IdP that JSS can reach can issue tokens for any WebID claim. A hostile IdP at
evil.examplecould mint a token withwebid: https://alice.solidcommunity.net/profile/card#meand JSS would accept it. The profile check is what binds tokens to the user's declared trust set.Portability theatre: Counterintuitively, the missing check means cross-pod auth currently "works" — tokens from any IdP are accepted. Fixing this properly will tighten the check: profiles must declare their IdPs via
solid:oidcIssuer. For the common case (the IdP is the same pod that hosts the profile) this is one extra HTTP GET; the cost is real but small.Fix sketch
Inside the token-verify path (after the
webidis extracted, before the success return):Cache the profile parse (5–15 min TTL) — every request would otherwise re-fetch the profile, which is expensive at scale.
Why I'm filing
The
solid-apps/exploreris now multi-pod (sidebar + split mode in PRs #6/#7). The natural follow-up was "do I need to log into both pods?" — Solid-OIDC's whole point is "no, you don't". JSS gets most of the way there; this is the last spec-completeness gap before portable cross-pod auth is genuinely safe.Out of scope
issclaim value lookup (already handled correctly)src/idp/) — this issue is RS-onlyRelated
did:nostr) is a different, simpler trust model that sidesteps this entirely by binding identity to a keypair instead of an IdP federation