Node-solid-server: 'aud' claim in Solid OAuth tokens

Created on 23 Jan 2019  Â·  52Comments  Â·  Source: solid/node-solid-server

The aud claim in OAuth tokens issued by Solid appears incorrect and is causing authentication errors when a client supplies the access token.


_Added 2019-Feb-01:_

_Added a TLDR re-iteration of the essential problem which triggered this issue, and the proposed fix, here for those not wanting to read the complete thread (though I recommend it, there's a lot of good information courtesy of @dmitrizagidulin)_


@RubenVerborgh @kjetilk @dmitrizagidulin: Please could you comment?
/cc @kidehen @smalinin

Manifestation

OpenLink are seeing error Token does not pass the audience allow filter when trying to bind a Solid pod to Virtuoso WebDAV using our WebDAV 'DET' functionality.

Recreation

I've been able to replicate the problem, eliminating Virtuoso in the process.

I signed into a local instance of OSDB using solid.openlinksw.com:8444, then while logged in, I retrieved the access token from OSDB's oidc-rp storage.

curl -kIH  "Authorization: Bearer {access-token}" https://cmsblakeley.solid.openlinksw.com:8444/public/

HTTP/1.1 403 Forbidden
X-Powered-By: OpenLink Solid Server
Vary: Accept, Authorization, Origin
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, WAC-Allow, Content-Length, WWW-Authenticate, On-Behalf-Of, webid-tls
Allow: OPTIONS, HEAD, GET, PATCH, POST, PUT, DELETE
WWW-Authenticate: Bearer realm="https://solid.openlinksw.com:8444", error="access_denied", error_description="Token does not pass the audience allow filter"
Content-Type: text/html; charset=utf-8
Content-Length: 877
ETag: W/"36d-HJZUCkFVG9xNWz4IGHiCqJijAlc"
Date: Tue, 22 Jan 2019 17:05:34 GMT
Connection: keep-alive

Decoding the oauth token (a JWT) using https://jwt.io/

{
  "iss": "https://solid.openlinksw.com:8444",
  "sub": "https://cmsblakeley.solid.openlinksw.com:8444/profile/card#me",
  "aud": "79888c62ca35e4b89665f328b494600b",
  "exp": 1549385146,
  "iat": 1548175546,
  "jti": "5709a267c48596e8",
  "scope": "openid profile email"
}

The "aud" (Audience) claim matches the clientId under key rpConfig.session.clientId in the oidc-rp data.

Problem Source

(Line numbers refer to OpenLink's forks)
The error comes from: AuthenticatedRequest#allowAudience

@solid/oidc-rs/src/AuthenticatedRequest.js:354

  allowAudience (request) {
    ...
    if (typeof audience === 'function') {
      if (audience(aud)) {
        return  // token passes the audience filter test
      } else {
        return request.forbidden({
          realm,
          error: 'access_denied',
          error_description: 'Token does not pass the audience allow filter'
        })
      }
    }
    ...

Function audience above is a reference to OidcManager#filterAudience

@solid/oidc-auth-manager/src/oidc-manager.js:251

  initRs () {
    let rsConfig = { // oidc-rs
      defaults: {
        handleErrors: false,
        optional: true,
        query: true,
        realm: this.providerUri,
        allow: {
          // Restrict token audience to either this serverUri or its subdomain
          audience: (aud) => this.filterAudience(aud)
        }
      }
    }

    this.rs = new ResourceAuthenticator(rsConfig)
  }

The filter function which is rejecting the aud claim is defined at:

@solid/oidc-auth-manager/src/oidc-manager.js:441

  filterAudience (aud) {
    if (!Array.isArray(aud)) {
      aud = [ aud ]
    }

    return aud.some(a => OidcManager.domainMatches(this.providerUri, a))
  }

The comments for method domainMatches state:

  * Tests whether a given Web ID uri belongs to the issuer. They must be:
  *   - either from the same domain origin
  *   - or the webid is an immediate subdomain of the issuer domain

There's a mismatch between the form of the aud claim in the supplied access token and the form expected by OidcManager#domainMatches, i.e. clientId as opposed to webID. (We confirmed this in-house by adding extra debug output. The aud claim being tested by filterAudience() is a clientId, not a webID).

Fix?

According to RFC7519: JSON Web Token (JWT) - Section 4.1.3 "aud" (Audience) Claim

The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.

OpenID Connect Core 1.0

In an ID token issued to a client, the aud claim must contain the client_id.

aud
REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string.

But, in the case of an access token, rather than an ID token, as far as I can see from the OIDC spec' the client doesn't use the aud claim at all when validating any access token it receives.

Given the definition of aud in the JWT spec' ('The "aud" (audience) claim identifies the recipients that the JWT is intended for.'), it seems to me that the access tokens issued by Solid need to be changed so that the aud field includes the iss claim or the sub claim, instead of (or in addition to) the client_id.

triage

Most helpful comment

@cblakeley

Solid pods can be accessed using a regular access token, or can they only be accessed using a PoP token?

Yeah. So, like you gathered from the code, they can be accessed with a non PoP-based access token. But!

am I right in thinking the access token in the client's oidc-rp storage can be used as a 'regular' access token?

So that right there is basically the problem. The access_token in the oidc-rp client's storage is kind of useless, at the moment, as a regular access token (for the pod). This came about for a couple of reasons:

  • Neither the OAuth2 or the OIDC spec has anything to say about the format of an access token. I'm not sure why this is, but in fact the specs insist that access tokens are "opaque", which means they don't say anything about em. (They have lots to say about the structure of _ID Tokens_, but not access tokens). I find this is not helpful, because what actually ends up happening in implementations, is that _almost everybody_ uses some sort of JWT, but not standardized, as an access token.
  • At the time of implementation, the main focus was on the ID Tokens, getting them right, and having them work with PoP tokens for multi-RS use cases.
  • Usually, access tokens are intended to carry scope - which determines fine grained access control, etc. Except that in Solid, scope (of the kind used by OAuth2) was kind of useless (since we have Web Access Control). Btw, I hope to change this some time soon - and to propose a way to integrate WAC and token scope and the ACL system.
  • So, because access tokens weren't spec'd, and because we were focused elsewhere, we implemented Access Tokens to be very similar in structure to ID tokens -- similar fields and issuing logic. Which mostly works out OK, except for one detail - for ID Tokens, aud gets set to the RP's client_id, as per the spec. But Access Tokens are much more useful when their aud is set to the _Resource Server_, not the RP.
  • (In fact, I was largely tempted to leave access tokens out of the implementation of solid's auth client. Except I figure, eh, why not leave them in, maybe we can do something with them later, and integrate scope and WAC.)

So anyways, what it sounds to me is that these should be the next action steps, to prevent these kind of aud errors from happening to developers:

  1. We add a lot more documentation :) (Specifically, document the multi-RS workflow and PoP token usage in the webid-oidc-spec repo, but also have some code examples of how to use tokens with curl and so on, to cover common developer usage.)
  2. We change the way Access Tokens are issued by default, and set their audience to the Resource Server (instead of client_id). That way, the access_token in oidc-rp storage can actually be used as a simple way to communicate with a user's home pod.
  3. (Someday/maybe) Write up the multi-RS PoP-based workflow as an IETF draft, and start the standards process of putting it through the IETF OAuth2 work group. (This is less related to the aud issue, but would be just nice in general.)

All 52 comments

@RubenVerborgh @kjetilk @dmitrizagidulin: Please could you comment?

I can't comment beyond "that's quite possible" 🙂
Have not written any OIDC code myself.

Ok, so, this is on purpose, and has to do with Solid's extension to OIDC to make it more decentralized (to allow an app to authenticate to multiple unrelated Resource Servers with just one ID token). I'll explain more in another comment, but just wanted to give a heads up.

@dmitrizagidulin,

What's on purpose? How does this extend decentralization?

The filter function which is rejecting the aud claim is defined at:

@solid/oidc-auth-manager/src/oidc-manager.js:441

  filterAudience (aud) {
    if (!Array.isArray(aud)) {
      aud = [ aud ]
    }

    return aud.some(a => OidcManager.domainMatches(this.providerUri, a))
  }

The comments for method domainMatches state:

  * Tests whether a given Web ID uri belongs to the issuer. They must be:
  *   - either from the same domain origin
  *   - or the webid is an immediate subdomain of the issuer domain

I think another question about this then, for @dmitrizagidulin, is whether this check is correct. Why is this checked on audience rather than subject?

Also, this check seems obsolete given the oidcIssuer predicate, because my WebID is on ruben.verborgh.org whereas my pod is on drive.verborgh.org. So the latter is an issuer for the former, but the check would fail.

@RubenVerborgh,

We have fixed this issue and published a PR.

What next?

@kidehen Determining whether that PR is indeed fixing the correct issue; see follow-up in https://github.com/solid/oidc-op/pull/13.

Hi everyone! So, the root cause for this issue is lack of documentation on our part.

The short version is - the access token from oidc-rp's storage (I assume it got there via solid-auth-client) is not something you can use directly for requests to the server. It's meant to be used through the auth client's popTokenFor() method, and is used for Solid's WebID-OIDC protocol, which is a combination of regular OIDC and Proof of Possession tokens.

It _is_ possible to request regular OIDC ID tokens from a solid server (by passing in a popToken: false to the RelyingParty constructor), but by default solid-auth-client requests a different kind of token (for use inside its authenticated fetch logic). And on the receiving end, oidc-rs supports both kind of tokens.

So that's the short version - the audience claim is not what you would expect from an OIDC type access token, and that's on purpose. In the next comment, let's get into the broader context of how one uses PoP tokens, and why we need both types of tokens in Solid.

Ok, so what are these Proof of Possession tokens for?

Normally, OAuth2 and OpenID Connect require that a token is _restricted to a particular resource server_ - that's what the aud: claim is for. This is done for excellent reason - see for example the Bind Tokens to a Particular Resource Server (Audience) section of the OAuth 2.0 Threat Model and Security Considerations spec. In the early days, various identity providers found out the hard way that if you don't restrict the audience, it's way too easy for a rogue resource server to steal the token and use it to impersonate the user on other systems (with other resource servers).

So that's why OIDC requires the aud parameter in its tokens: "aud REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value."
Here's how audience restriction normally works. Lets say I want to view a protected resource on Solid.community, and I'm using my own pod, dmitri.com (not a real pod) as an identity provider.
In this interaction, the actors are:

Identity Provider (IdP): dmitri.com
Relying Party (RP): solid.community
Resource Server (RS): solid.community

I go to access the resource, solid.community prompts me to authenticate, I indicate that dmitri.com is my pod. Classic OIDC workflow. Notice, btw, that solid.community is acting as _both_ a Relying Party and a Resource Server.
So, solid.community makes a request to my pod, dmitri.com, and says "Please give me a token for this user. It's for my own use".
The IdP issues the token, and restricts the audience of its token only to the RP (specifically, to its client_id). So the token looks something like:

{
  sub: "https://dmitri.com/profile/card#me",  // <- my Web ID
  iss: "https://dmitri.com",
  aud: "https://solid.community",  // (it's actually solid.community's client_id)
  // ... other stuff
}

Makes sense so far, yeah? The token is tightly scoped to one resource server, so solid.community can't secretly take it and impersonate me to some other server (because that other server will check the aud: param, and reject it).

(to be continued)

Ok, so that was an over-simplified example, to illustrate what aud:ience filtering is _for_ -- it's a mechanism to prevent resource servers from reusing your tokens elsewhere. So each Solid server inspects bearer tokens and _only allows tokens that were minted with it as the audience_.

So, as pointed out previously, in oidc-auth-manager, when the server starts up, the Resource Server portion of its config looks like:

  initRs () {
    let rsConfig = { // oidc-rs
      defaults: {
        realm: this.providerUri,
        allow: {
          // Restrict token audience to either this serverUri or its subdomain
          audience: (aud) => this.filterAudience(aud)
        }
      }
    }
   // ...

(And the filterAudience() function just checks "is this token intended for this server, or its subdomain?"). In other words, the server solid.community will _only_ allow tokens which have either https://solid.community in their aud: claim, or any of its subdomains, liks https://*.solid.community.

Incidentally, the subject has nothing to do with this mechanism, and _must not be included_ -- oidc-op PR #13 cannot be accepted, as it bypasses the whole audience filtering mechanism.

Ok, but back to the original problem -- why was the error Token does not pass the audience allow filter encountered in the first place? Why was the aud: claim of that token not https://solid.openlinksw.com:8444 but instead was 79888c62ca35e4b89665f328b494600b, which was the clientId of whatever web app was using it to authenticate?

So now we're back to Proof of Possession tokens.

(to be continued)

The classic OAuth2 / OIDC workflows were designed for situations where there's only _two_ "trust zones".

For example, say you have a third-party Twitter client app (basically what OAuth was invented for). In this case, the trust zones are: 1) Twitter (playing the role of _both_ the Identity Provider and the Resource Server), and 2) the third party app, playing the role of Relying Party.

Or take the example of typical "social media login", where you can log into some web service using your Github account. There's _still_ only two trust zones - Github is playing the role of the IdP, and the web service is playing the role of both the Relying Party and Resource Server. And similarly, in our simplified dmitri.com / solid.community example above, the latter played the role of both RP and RS.

The reason I bring these examples up, is to point out that the audience filtering is only useful for situations when the IdP _knows which Relying Party/Resource Server_ the token is intended for. Or to put it another way, audience filtering is only practical when _either_ the IdP issues tokens for _itself_ (like with the Twitter client app example), _or_ when the RP is also the Resource Server (so it knows to ask for a scoped token for its own use).

But the unique challenge that Solid apps face (not encountered in the traditional OIDC world), is that a Relying Party has to interact with _an arbitrary amount_ of Resource Servers outside its trust zone.

Consider a Solid-style DecentralizedTwitter app. I go to use the app. I authenticate to it (selecting dmitri.com as my pod / identity provider). But now the app has to deal with potentially hundreds of other pods (of people whose feeds I follow)! So instead of two trust zones, we're dealing with N+2 -- the IdP, the app, and N independent Resource Servers.

So this is the main thing differentiating Solid's WebID-OIDC authn protocol from regular OIDC. The necessity to deal with an arbitrary amount of independent resource servers.
Just to be clear, each Solid pod is a classical OIDC OP (identity provider), so it can request and accept regular OIDC style ID tokens (where the RP/RS is known in advance).

But how do we deal with the decentralized twitter use case? Let's zoom in.

We have the user's pod, playing a traditional IdP role. No problem there.

Then we have the DTwitter app, which is the Relying Party. (Actually, it's technically something new, in this situation, it's a _presenter_ role, but ignore that for now.)

And then we have N Resource Servers, different domains and trust zones from the pod or the app.

So the app has to make authenticated requests to those N Resource Servers. And each request has to carry a token that is audience-bound _only to that server_.

What are our options?

We can't _not_ include an audience binding. Meaning, imagine there was a way for the app to say "hey pod, please give me a general-purpose ID Token that's good for any resource server". That would be really simple, but also wildly insecure, since any of those resource servers could then take that token and pretend to be the user (since there's no audience binding).

Should we then require that for each new Resource Server request it makes, the app should go through the auth flow with the identity provider? (to request a token bound just to that RS?). So like, if I follow Alice and Bob on my DTwitter feed, should the app request "pod, please give me a token bound to Alice.com" and also "Oh and now give me a token bound to Bob.com"?
Well that clearly is not going to work, that's wayy too many round-trip flows.

So, this is the design space and requirements in which WebID-OIDC was developed.

(to be continued tomorrow.)

@dmitrizagidulin,

What are the implications of the fix that @cblakeley submitted? Do you concur with the fix? Or do you concur with claim by @RubenVerborgh that said fix addresses a symptom rather than baseline problem?

@kidehen The implications are - the PR breaks audience filtering, and should be closed. The baseline problem is in the app logic, using the token that's meant for embedding as a standalone token. I'll give code examples on how to get OIDC tokens via the auth client (so they can be passed in requests), and how to use the existing PoP tokens the auth client creates by default.

@dmitrizagidulin Thanks for the explanation so far, we should copy that into the docs. Looking forward to the next part. (Wild guess: this is probably where the client creates a token for an external resource server.)

@dmitrizagidulin Something I don’t understand with the explanation above is OidcManager.domainMatches(this.providerUri, a).

Why are we checking the (identity??) _provider_? I would, based on the above, expect a check of whether the token can be used this on (subdomains of) the resource server. The code comment mentioned above is also confusing in that regard. What is an expected example value in the aud field?

(No need to answer directly; just hope to find these answers in your continuation.)

@kidehen The implications are - the PR breaks audience filtering, and should be closed. The baseline problem is in the app logic, using the token that's meant for embedding as a standalone token. I'll give code examples on how to get OIDC tokens via the auth client (so they can be passed in requests), and how to use the existing PoP tokens the auth client creates by default.

So you are going to produce a code snippet about the expected behavior of a generic OIDC Relying Party that encounters this problem?

Or are you going to produce a curl command sequence (based on what @cblakeley provided above) that results in a different outcome i.e., no error based on the token presented?

The baseline problem is in the app logic, using the token that's meant for embedding as a standalone token.

To what do you refer by the phrase "App Logic" re this problem scenario?

I ask because @cblakeley provides an example using curl .

@RubenVerborgh

Wild guess: this is probably where the client creates a token for an external resource server.

Hehe, yes, exactly! :)

Why are we checking the (identity??) provider? I would, based on the above, expect a check of whether the token can be used this on (subdomains of) the resource server

So, I think part of the confusion comes from the fact that in node-solid-server, the identity provider uri is the same as the resource server uri! So it's actually checking whether the token can be used on the domain of the resource server, just like you say.

@kidehen

Or are you going to produce a curl command sequence

Sure, I can do that. There's basically another function call or two required to produce a token that's usable with curl. It's not sufficient just to get it from storage.

@cblakeley @kidehen
The relevant code for getting a token usable with curl can be found here: solid-auth-client/fetchWithCredentials()

The relevant line is:
const popToken = await PoPToken.issueFor(toUrlString(input), session)
which issues a token for a given Resource Server url (that's the input), which you can then print on the console and use in curl.

But there's probably easier ways to get a token, depending on what you're trying to do.

Thanks a lot for the explanation so far, @dmitrizagidulin ! This is very good stuff, I'll hang around for the rest! :100:

So, I think part of the confusion comes from the fact that in node-solid-server, the identity provider uri is the same as the resource server uri! So it's actually checking whether the token can be used on the domain of the resource server, just like you say.

@dmitrizagidulin could you please document it for general scenario where we have IdP and pods and apps on completely different domains? Later one can apply it to special scenarios which has IdP and pod on the same domain.

  • IdP

    • borring-idp.example (for Alice & Bob)

    • dramatic-idp.example (for Romeo & Juliet)

  • pods (resource servers)

    • alice.acme.example

    • bob.ajax.example

    • romeo.montague.example

    • juliet.capulet.example

  • apps

    • calendar.foobar.example

    • chat.escalus.example

So instead of two trust zones, we're dealing with N+2 -- the IdP, the app, and N independent Resource Servers.
[...]
So, this is the design space and requirements in which WebID-OIDC was developed.

:+1: 🤯

@dmitrizagidulin: Thanks for the very detailed response! Lots to digest...

@cblakeley @kidehen
The relevant code for getting a token usable with curl can be found here: solid-auth-client/fetchWithCredentials()

The relevant line is:
const popToken = await PoPToken.issueFor(toUrlString(input), session)
which issues a token for a given Resource Server url (that's the input), which you can then print on the console and use in curl.

Here's what I am gleaning from all of this:

  1. WebID-OIDC as a protocol isn't simply about adding an solid:oidcIssuer relation lookup to the basic OIDC protocol

  2. Token access is part of the protocol which also entails additional Relying Party and Identity Provider exchanges i.e., access from local storage isn't acceptable.

If what I've outlined is correct, why isn't it documented?

/cc @cblakeley @RubenVerborgh @smalinin

@dmitrizagidulin

Identity provider URI versus server URI

in node-solid-server, the identity provider uri is the same as the resource server uri!

That is not always the case, right?
Even your simple example above is a counterexample:

Identity Provider (IdP): dmitri.com
Relying Party (RP): solid.community
Resource Server (RS): solid.community

So is the providerUri in OidcManager.domainMatches(this.providerUri, a) just a naming problem, or is something wrong?

Can popToken: false be the default?

It is possible to request regular OIDC ID tokens from a solid server (by passing in a popToken: false to the RelyingParty constructor)

Is it possible to have popToken: false by default, such that:
1) the token can be used as a regular OIDC token (on one server only)?
2) we _also_ can generate popTokens from it (for third-party resource servers)?

That second point is the tricky one. I understand the need for popTokens in general, I just wonder whether we can kill two birds with one stone here.

@cblakeley Thanks! And I very much appreciate all the work you've put in to studying Solid authn, and reading the specs! The Solid community definitely needs more experts on this.

@kidehen

why isn't it documented?

You're absolutely right, it definitely should be documented.

@RubenVerborgh OH! :) I see what you're asking! Yes, that's definitely just a naming problem. The variable providerUri just happened to be conveniently nearby, and initialized from config.serverUri. So yes, that should probably be renamed. (Sheesh, I didn't even notice it, my mind automatically translated to serverUri :) )

Is it possible to have popToken: false by default, such that:

Yes, no reason not to. (In fact, popToken is actually false by default, in the oidc-rp/RelyingParty constructor.) solid-auth-client seems to bypass that, though, and always invokes PopToken.issueFor() (in its authenticated fetch).
Anyways, this should definitely be streamlined (I have some thoughts on this, below).

All right, so, let's wrap up the explanation, for completeness.

So we have these conflicting design constraints:

  1. Tokens should be audience-bound to their intended recipients (RelyingPart or ResourceServer), to prevent token misuse and impersonation.
  2. Solid apps have the unusual requirement (though it seems to be core to Solid's premise) that one app needs to make authenticated requests to an arbitrary number of Resource Servers (that can potentially be unrelated to a user's pod or the app's server). And, by the previous item, those requests all need to be audience-bound to each different RS.
  3. Tokens are typically issued (and signed) by the user's pod, except that having an app make a separate HTTP request (let alone a separate OIDC authorization flow) for each token (intended for another Resource Server) quickly gets to be impractical and prohibitive. (And getting a whole bunch of tokens up front, in a batch, is also difficult, due to discovery and link hopping inherent in linked data workflows.)

Fortunately, there was a set of IETF specs (written by some of the authors of OAuth2 and OpenID Connect), that worked with OIDC tokens and could satisfy the design constraints laid out above:

Here's how these combined with OIDC in Solid.

1) During the OIDC Authentication Request (using either the Implicit or the Authorization Code flow), the Relying Party auth client generates a public/private pair of ephemeral session keys.

The RP auth client sends the public key from this pair in the cnf ("confirmation") parameter of the Authentication Request, thereby registering this key with the pod/Identity Provider. (It actually passes the cnf claim in the request parameter, but that was done for convenience; it just as easily could pass it in a standalone cnf url parameter, along with nonce etc.)

Note: This step is optional (and only used if you want to be able to generate PoP tokens later on). If the RP client doesn't generate and send a cnf key with the Auth Request, then the workflow proceeds as plain OIDC.

2) On a successful Token Response, the pod _also_ includes the cnf key in the resulting token that it hands to the auth client. That is, it's a regular ID Token, but it contains the extra cnf claim in it.

This essentially turns a regular ID Token into a Ticket Granting Ticket (to use Kerberos terminology).

To put it antoher, way, the auth client says "Pod, please give me a session token. And here's my temporary public key, so I can do Proof of Possession proofs to any downstream Resource Server. (This will prove that the token wasn't stolen by some other auth client.)"

The pod/IdP answers "Here is your ID token, good for a limited time, and contains the user's WebID etc. This token is audience-bound to you, the auth client, and also includes your public key in it, to show that it's been registered with me."

(to be continued)

@dmitrizagidulin,

Are you confirming the following claim from my earlier post?

WebID-OIDC as a protocol isn't simply about adding an solid:oidcIssuer relation lookup to the basic OIDC protocol

I assume yes, since you've basically indicated the existence of an undocumented Solid Extension to OIDC that has basically come to light via this use-case, right?

Rather than WebID-OIDC, we actually have: WebID-Solid-OIDC i.e., two significantly different protocols.

/cc @RubenVerborgh @cblakeley

@kidehen I don’t quite understand your claim, to confirm it. Webid-OIDC was never claimed to be “simply” anything. And the lack of documentation for the multi-rs workfow has been an acknowledged reality among the solid team for a long time...

@kidehen I don’t quite understand your claim, to confirm it. Webid-OIDC was never claimed to be “simply” anything. And the lack of documentation for the multi-rs workfow has been an acknowledged reality among the solid team for a long time...

Okay, let me be more precise:
I assumed that WebID-OIDC was a protocol that extended OIDC by adding a WebID-Profile relation (i.e. solid:oidcIssuer) look-up to the standard protocol.

Until this issue arose, the statement above held true fundamentally i.e., a WebID is added to OIDC tokens which is then used for the de-reference and solid:oidcIssuer relation lookup.

For purposes of analogy solely (as I really do no want to bring TLS into this thread), the WebID-TLS protocol is basically standard TLS plus a WebID-Profile doc relation (i.e., cert:key) lookup too i.e., said lookup follows successful completion of a standard TLS-handshake.

You now indicate via this thread that Solid has an OIDC extension which is quite a different thing all together as this case has unveiled i.e., it isn't standard OIDC.

The magnitude of this difference shouldn't have been left undocumented, assuming I am not overlooking some existing documentation about this protocol implementation.

BTW -- is there a link to something about multi-rs workfow and its documentation shortcomings that I can read?

@dmitrizagidulin: Still reading. I removed my previous comment as a result ... Is the following correct?

A PoP token is a signed JWT containing the client's access token and request specific claims. The access token must contain the client's public key in the cnf claim. Because a PoP token contains request specific claims, one must be generated afresh for each request. PoPToken#issueFor() (or RelyingParty#popTokenFor which is a wrapper around it) is the mechanism to do this and must be called by the client prior to each request to the RS.

In order to ensure the access token contains the client's (RP's) public key, the client's authentication request to the OP should include the RP auth client's public key in the cnf claim.

/cc @kidehen @RubenVerborgh

@dmitrizagidulin: I'm trying to get a clear understanding of what needs to be changed in Virtuoso to allow it to mount and access Solid pods via its WebDAV extensions. Sorry for more questions...

  • When you commented above "And on the receiving end, oidc-rs supports both kind of tokens" does this mean that Solid pods can be accessed using a regular access token, or can they only be accessed using a PoP token? From a quick look at oidc-rs (AuthenticatedRequest:validateAccessToken and AuthenticatedRequest:validatePoPToken), use of PoP tokens appears optional.
  • If they can be accessed using a regular access token (am I right in thinking the access token in the client's oidc-rp storage can be used as a 'regular' access token?), which is what I tried to simulate in the curl example at the start of this issue's thread, is the main issue then something going awry with the audience filtering.
  • If use of PoP tokens is optional, I'm not clear why the initial curl example failed. You said earlier that the audience filtering is working?

If use of PoP tokens is mandatory for pod access then OpenLink has work to do to modify Virtuoso to use PoP tokens.

/cc @kidehen @RubenVerborgh

@cblakeley Excellent questions! I'll try to answer these shortly.

(Also, I totally hear you, those PoP specs are a headache to read! :) Took me sooo many tries!)

@dmitrizagidulin OK, thanks. In the meantime, please could you give a quick yes or no as to whether PoP tokens are mandatory with Solid?

@cblakeley Sure - definitely not mandatory.

@dmitrizagidulin ,

If they ("PoP Tokens") aren't mandatory why do we have an issue here?

The code implementation treats them as mandatory, hence the problem.

Am I missing something here, logically?

/cc @cblakeley @RubenVerborgh

Should we then require that for each new Resource Server request it makes, the app should go through the auth flow with the identity provider? (to request a token bound just to that RS?). So like, if I follow Alice and Bob on my DTwitter feed, should the app request "pod, please give me a token bound to Alice.com" and also "Oh and now give me a token bound to Bob.com"?
Well that clearly is not going to work, that's wayy too many round-trip flows.

[...]

To put it antoher, way, the auth client says "Pod, please give me a session token. And here's my temporary public key, so I can do Proof of Possession proofs to any downstream Resource Server. (This will prove that the token wasn't stolen by some other auth client.)"

[...]

@dmitrizagidulin OK, thanks. In the meantime, please could you give a quick yes or no as to whether PoP tokens are mandatory with Solid?

@cblakeley Sure - definitely not mandatory.

Without PoP tokens, for each Resource Server, does the app needs to go through the auth flow with the identity provider?

@elf-pavlik

Without PoP tokens, for each Resource Server, does the app needs to go through the auth flow with the identity provider?

@kidehen

The code implementation treats them as mandatory,

So, lemme clarify -- PoP tokens are not mandatory in the sense that you can point an off-the-shelf OIDC client at a Solid server, and that client will get a valid ID token, etc. Meaning, it interops with OIDC just fine.
Now, if you wanted to build an app that interacted with an arbitrary amount of Resource Servers without PoP tokens? I'm not sure how you'd accomplish that.

@cblakeley

Solid pods can be accessed using a regular access token, or can they only be accessed using a PoP token?

Yeah. So, like you gathered from the code, they can be accessed with a non PoP-based access token. But!

am I right in thinking the access token in the client's oidc-rp storage can be used as a 'regular' access token?

So that right there is basically the problem. The access_token in the oidc-rp client's storage is kind of useless, at the moment, as a regular access token (for the pod). This came about for a couple of reasons:

  • Neither the OAuth2 or the OIDC spec has anything to say about the format of an access token. I'm not sure why this is, but in fact the specs insist that access tokens are "opaque", which means they don't say anything about em. (They have lots to say about the structure of _ID Tokens_, but not access tokens). I find this is not helpful, because what actually ends up happening in implementations, is that _almost everybody_ uses some sort of JWT, but not standardized, as an access token.
  • At the time of implementation, the main focus was on the ID Tokens, getting them right, and having them work with PoP tokens for multi-RS use cases.
  • Usually, access tokens are intended to carry scope - which determines fine grained access control, etc. Except that in Solid, scope (of the kind used by OAuth2) was kind of useless (since we have Web Access Control). Btw, I hope to change this some time soon - and to propose a way to integrate WAC and token scope and the ACL system.
  • So, because access tokens weren't spec'd, and because we were focused elsewhere, we implemented Access Tokens to be very similar in structure to ID tokens -- similar fields and issuing logic. Which mostly works out OK, except for one detail - for ID Tokens, aud gets set to the RP's client_id, as per the spec. But Access Tokens are much more useful when their aud is set to the _Resource Server_, not the RP.
  • (In fact, I was largely tempted to leave access tokens out of the implementation of solid's auth client. Except I figure, eh, why not leave them in, maybe we can do something with them later, and integrate scope and WAC.)

So anyways, what it sounds to me is that these should be the next action steps, to prevent these kind of aud errors from happening to developers:

  1. We add a lot more documentation :) (Specifically, document the multi-RS workflow and PoP token usage in the webid-oidc-spec repo, but also have some code examples of how to use tokens with curl and so on, to cover common developer usage.)
  2. We change the way Access Tokens are issued by default, and set their audience to the Resource Server (instead of client_id). That way, the access_token in oidc-rp storage can actually be used as a simple way to communicate with a user's home pod.
  3. (Someday/maybe) Write up the multi-RS PoP-based workflow as an IETF draft, and start the standards process of putting it through the IETF OAuth2 work group. (This is less related to the aud issue, but would be just nice in general.)

@cblakeley - Oh, I missed your earlier comment!

A PoP token is a signed JWT containing the client's access token and request specific claims.

Yes! (Except it contains the ID Token, not the access token.)

The ~access~ _ID_ token must contain the client's public key in the cnf claim.

Yep.

Because a PoP token contains request specific claims, one must be generated afresh for each request. PoPToken#issueFor() (or RelyingParty#popTokenFor which is a wrapper around it) is the mechanism to do this and must be called by the client prior to each request to the RS.

Yes, exactly. (And actually, the only thing that differs for each request is the aud is specific to the targeted Resource Server. So, aud is the only request-specific claim.)

In order to ensure the ~access~ ID token contains the client's (RP's) public key, the client's authentication request to the OP should include the RP auth client's public key in the cnf claim.

Yes.

Added a reference to this comment at the start of this thread

TLDR version

(or at least my interpretation)

From: https://github.com/solid/node-solid-server/issues/1061#issuecomment-459574596

Core problem triggering this issue

The access_token in the oidc-rp client's storage is kind of useless, at the moment, as a regular access token (for the pod).

Key proposed action

Change the way Access Tokens are issued by default, and set their audience to the Resource Server (instead of client_id). That way, the access_token in oidc-rp storage can actually be used as a simple way to communicate with a user's home pod.

@cblakeley Perfect, thanks :)

Change the way Access Tokens are issued by default, and set their audience to the Resource Server (instead of client_id). That way, the access_token in oidc-rp storage can actually be used as a simple way to communicate with a user's home pod.

Which Resource Server you would set it to? The one that hosts WebID Profile? Given that every person can use any number of Resource Servers (personal, organizational etc.) person might even in WebID Profile have pim:storage pointing to another server, and type indexes can also have registrations pointing to any number of resource servers. If we make some Resource Server special in issued access_token, we need to make sure that applications will not make any assumptions about this Resource Server and always follow https://github.com/solid/solid-spec/blob/master/solid-webid-profiles.md#account-resource-discovery

@elf-pavlik ok, so, think of the access_token as a shortcut / optimization. It's a way to simplify the common use case where a pod is both an IdP and a Resource Server. So, in the auth response, the IdP says "here's an ID token to identify you to any number of resource servers you may want to interact with (via PoP tokens). And here's also an access_token that you can use to communicate (without wrapping in a PoP token) with the one Resource Server that I happen to control."

So, for all those other Resource Servers that you mention - that's what the ID Token is for.

To put it another way - the access_token is there for off-the-shelf OIDC clients. (Who always make the implicit assumption that the access token is for the RS that the IdP controls.)
All Solid clients (who have to deal with a multitude of RS possibilities) should use PoP tokens.

@dmitrizagidulin,

This thread was instigated based on issues that came to light in our Virtuoso product (an off-the-shelf OIDC Client in this scenario) and its integration with Solid Pods deployed in OIDC-mode i.e., mounting them as it does other data data spaces (e.g., S3, Dropbox, Box, OneDrive, and many others based on relevant open standards).

Do you have a fix ETA?

/cc @RubenVerborgh @cblakeley

To put it another way - the access_token is there for off-the-shelf OIDC clients. (Who always make the implicit assumption that the access token is for the RS that the IdP controls.)
All Solid clients (who have to deal with a multitude of RS possibilities) should use PoP tokens.

If you introduce such special case I think documentation needs to very clearly warn app developers not to naively use it in their apps, which would lead to those apps only working with some subset of deployments which fit into this special case.

Change the way Access Tokens are issued by default, and set their audience to the Resource Server (instead of client_id). That way, the access_token in oidc-rp storage can actually be used as a simple way to communicate with a user's home pod.

Could you please formulate a separate issue around that, @cblakeley, as this issue is now quite broad in scope ? It sounds like something we should have in for 5.0.0.

@kjetilk: Although there's a lengthy discussion above, isn't the core problem summarized briefly in the TLDR comment referenced at the start of this thread? If a new issue is needed, I think @dmitrizagidulin is best placed to raise it, as he proposed the key tasks arising from this issue and is most likely the one responsible for actioning them.

Created issue https://github.com/solid/node-solid-server/issues/1082

My I please ask to _not_ CC me on anything OIDC-related in the future? I have neither the knowledge nor the bandwidth to follow up adequately.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nicola picture nicola  Â·  4Comments

ludwigschubi picture ludwigschubi  Â·  3Comments

RubenVerborgh picture RubenVerborgh  Â·  3Comments

JornWildt picture JornWildt  Â·  6Comments

SvenDowideit picture SvenDowideit  Â·  6Comments