Skip to content

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

@melvincarvalho

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions