Skip to content

Solid-OIDC RS: missing solid:oidcIssuer profile check (steps 4–5 of the spec) #510

Description

@melvincarvalho

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:

  1. Fetch the WebID profile document over HTTP (with Accept: text/turtle, application/ld+json)
  2. Parse the RDF and verify that one of the profile's solid:oidcIssuer triples matches the token's iss claim
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions