solid / webid-oidc-spec

WebID-OIDC Authentication Spec v0.1.0
MIT License
56 stars 18 forks source link

Difficulty in implementing application flow #35

Open SimonShapiro opened 5 years ago

SimonShapiro commented 5 years ago

I use solid-auth-client as the mechanism to authenticate users and the app to access their PODs. I understand that once authenticated solid-auth-client places an object that is used later for interacting with their PODs into local storage. When I interact with the POD from the client using solid-auth-client everything works ok in spite of the app being built in Python and compiled to Javascript via Skulpt.

I am trying to re-use the solid-auth-client object from the browser by passing it on to the server so that the server can independently access the POD. There are many obvious use cases for this pattern. For example, I log in to the app and ask it to access my POD every night and send watermarked thumbnails of all new photos to an aggregator.

My choice of using Python wherever possible may have inter-op issues with the object in local storage, but theoretically shouldn't. I have been trying to follow the pattern set out in the updated spec for Sending a Request.

The authentication details are retrieved from localStorage with a javascript function:

  function getSolidAuthClient () {
    let solid_auth_client = JSON.parse(window.localStorage.getItem("solid-auth-client"))
    console.log({"localStorage": solid_auth_client, type:
                typeof(solid_auth_client)})
    return solid_auth_client
  }

When the authentication details are passed to the Python server from local storage, it is held in a variable called solid_auth_client. First I set up the pop_token as a json object:

  session = solid_auth_client["session"]
  pop_token = {
      "iss" : "https://solid-sparql.anvil.app",
      "aud" : "https://anvil1.inrupt.net", 
      "exp" : session["idClaims"]["exp"],
      "iat" : session["idClaims"]["iat"],
      "id_token" : session["authorization"]["id_token"],
      "token_type" : "pop"
    }

I then retrieve the private_key from solid_auth_client and convert it to PEM format:

  rehydrate = jwk.JWK.from_json(solid_auth_client["oidc.session.privateKey"])
  private_key = rehydrate.export_to_pem(private_key=True, password=None)

I use the PEM version of the key to sign the jwt of the pop_token:

  pop_token = jwt.encode(pop_token, private_key, algorithm="RS256")

Finally, I use the pop_token as an authorization header in the http GET from the PODs private area.

  try:
    resp = anvil.http.request(
      method = "GET",
      headers={
        "authorization": "Bearer "+pop_token.decode("utf-8") 
      },
      url="https://anvil1.inrupt.net/private/test.ttl" 
    )
  except Exception as e:
    print("HTTP error", e)

This results in HTTP error 401.

RubenVerborgh commented 5 years ago

So, let's take a step back to see what it is that you really want to do.

It seem to me that you want a server to be able to perform authenticated resource access, right?

Down the road, this will be addressed with OAuth2 application tokens. But this has not been implemented yet.

In the meantime, you can use this hack: https://github.com/jeff-zucker/solid-file-client


I am trying to re-use the solid-auth-client object from the browser by passing it on to the server

Security measures (token expiration) will purposely prevent you from doing that.

zenomt commented 5 years ago

Down the road, this will be addressed with OAuth2 application tokens.

can you elaborate or link to what you mean here? i'm not sure what an "OAuth2 application token" is. are you talking about offline refresh tokens, "service accounts", some kind of privileged meta-account that can act as another/any user at that OP, ...?

RubenVerborgh commented 5 years ago

Something like https://developer.twitter.com/en/docs/basics/authentication/overview/oauth.html

zenomt commented 5 years ago

@RubenVerborgh sorry, i still don't understand. assuming you're referring to the "Application-only authentication" mode on that page (since the "Application-user authentication" case is substantially the same as OIDC), for the decentralized Solid use case, wouldn't "app-only" access just be implemented by the app having a webid (that is, it's a bot), and granting access to the app's webid in ACLs? that also works for acl:AuthenticatedAgent cases, where you want some access token to access a substantially-public resource/API (perhaps for rate-limiting by webid). this (bots have webids) can be done today already, and could be done with less friction with #22. :)

also the "app-only" case on that page doesn't address the "app wants to act as the user even if the user is offline" situation, which i believe is the use case @SimonShapiro is talking about.

RubenVerborgh commented 5 years ago

To be honest, I'm not (trying to be) an expert in this matter; I'm merely echoing the high-level plan that I've heard.

Bots with WebIDs already sound good, but we haven't figured out the auth details for them (currently it's username/password with cookies).

But even if a bot has its own WebID, it would still be good if they could use something like an app token and secret, such that they act on behalf of a user.

So application-user authentication is the way (or something along those lines).

zenomt commented 5 years ago

Bots with WebIDs already sound good, but we haven't figured out the auth details for them (currently it's username/password with cookies).

all you need is for the app/bot's webid to list a solid:oidcIssuer URI that has a <URI>/.well-known/openid-configuration that answers with Content-type application/json, that has a jwks_uri entry that, when dereferenced, answers (with content-type application/json) a JWKS with a public key that your bot signs its own id_tokens with. the bot doesn't need to authenticate with its (potentially imaginary) OP because it just signs its own id_tokens (and POPTokens) without ever talking to an OP.

that should work today.

RubenVerborgh commented 5 years ago

Good point, indeed. Should work, just needs implementation.

zenomt commented 5 years ago

it looks like NSS (at least running on solid.community) only does an OPTIONS on the webid URI (presumably looking for the rel="http://openid.net/specs/connect/1.0/issuer" link), and does not actually retrieve the full profile document to look for the ?webid solid:oidcIssuer ?issuer triple.

this looks like an NSS bug. it will probably affect anyone whose webid document is hosted somewhere that didn't answer the expected Link to an OPTIONS (like me). specifically i saw this:

HTTP/1.1 401 Could not verify Web ID from token claims

and saw only this in my web server logs (duplicate fetches removed):

165.227.231.225 - - [11/Jul/2019:18:45:24 -0700] "GET /bot/oidc//.well-known/openid-configuration HTTP/1.1" 200 167 "-" "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" "zenomt.zenomt.com:443" 0.000 "-"
165.227.231.225 - - [11/Jul/2019:18:45:25 -0700] "GET /bot/oidc/jwks.json HTTP/1.1" 200 427 "-" "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" "zenomt.zenomt.com:443" 0.000 "-"
165.227.231.225 - - [11/Jul/2019:18:45:25 -0700] "OPTIONS /bot/card.ttl HTTP/1.1" 204 0 "-" "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" "zenomt.zenomt.com:443" 0.000 "-"

when trying to use my "bot" https://zenomt.zenomt.com/bot/card.ttl#me

and for the curious, here is the (expired) bearer token used with the above exchange:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImQyM2Y5ZmQ3LWYxYmItNGIzYi1hNjc2LTEzNjY2NjgxMmU4YSIsInRva2VuX3R5cGUiOiJwb3AiLCJqdGkiOiIzNjIwZGYzMi01NTM2LTRlMmQtODU5MS0zZjBhNmE0NjM0ZjkiLCJhdWQiOiJodHRwczovL3plbm9tdC5zb2xpZC5jb21tdW5pdHkiLCJleHAiOjE1NjI4OTYwNDMsImlhdCI6MTU2Mjg5NTkyMywiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS9vYXV0aC9jb2RlIiwiaWRfdG9rZW4iOiJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNklrc2lmUS5leUp1YjI1alpTSTZJakJtWkRZME9ETTRMV05qWlRFdE5ESTJPUzFoTmpOaExXWTBaRGRtWkdNeE9XWTJZaUlzSW5kbFltbGtJam9pYUhSMGNITTZMeTk2Wlc1dmJYUXVlbVZ1YjIxMExtTnZiUzlpYjNRdlkyRnlaQzUwZEd3amJXVWlMQ0poZFdRaU9sc2lZMnhwTFhSdmIyd2lMQ0pvZEhSd2N6b3ZMMkZ3Y0M1bGVHRnRjR3hsTDI5aGRYUm9MMk52WkdVaVhTd2ljM1ZpSWpvaWFIUjBjSE02THk5NlpXNXZiWFF1ZW1WdWIyMTBMbU52YlM5aWIzUXZZMkZ5WkM1MGRHd2piV1VpTENKcGMzTWlPaUpvZEhSd2N6b3ZMM3BsYm05dGRDNTZaVzV2YlhRdVkyOXRMMkp2ZEM5dmFXUmpMeUlzSW1OdVppSTZleUpxZDJzaU9uc2lhMlY1WDI5d2N5STZXeUoyWlhKcFpua2lYU3dpWlNJNklrRlJRVUlpTENKcmRIa2lPaUpTVTBFaUxDSmhiR2NpT2lKU1V6STFOaUlzSW00aU9pSTJMVEYxZFVwaVYyUXhjV05SWW5jeVYydFNhVGRvYm5wb1MyNDVkekp2Y1VSNVVEWlRXSFpOTjFkeU5tNUZSblU0UkRNd1kwMUdhbFZGVEY5M1kybEJTWFIxVWpKUVJWQlVTblJsZG5GWWEwTmhRbFI2TFRVMFVYUnlTekYzVWxScU4ySTVTM041WkRSWk4wUnZiRVJ3TVZVeFJtbEtObU16Y1hScGExVnhhbUppVTFReU1rb3djSGhMWWt4U2RHOVhSWFZ5V0VkWWJUVlRZVkZFYlMxSldsUnllRGROWmpSNlZXVTBWWFJqU2pORVRGbFZVVEJ4Vm1KbFFXbzRTV3MwTTBWM1lYVm1VblZIV0hOcFNqYzVSVlpQVFV0bVpUUlhVVTF3VFhsNExXUmtTemhqUzA0eFlqVjJORlJQZW5CWWVERjNOVmhaU2pVNFVsODBkR3RMWlRNNWVVeENjekY2VEZKeE4weE5SSEZ5YjBSNWRXaElTV2R6Y1ROd2JtNUNYMWxDWjJSeFFXVTNjR1pCYkZjNVR6aGFWWGRsVW1KRmJWOVNZVjkyWjNsNk1VNVBORWRVVmxOWk5rNHRjblJHZGtkVlYxRWlMQ0pyYVdRaU9pSkxJbjE5TENKaFkzSWlPaUl3SWl3aWFuUnBJam9pWkROaU5HVXhOR1l0T0dZMFpTMDBNalk1TFdFM05qY3RORE0wTkdFeVpESXdZelJqSWl3aVpYaHdJam94TlRZeU9EazVOVEl6TENKaGRYUm9YM1JwYldVaU9qRTFOakk0T1RVNU1qTXNJbUY2Y0NJNkltTnNhUzEwYjI5c0lpd2lhV0YwSWpveE5UWXlPRGsxT1RJemZRLnpfX2g1SC1RTlEzNWZSc0NLV2lLOG9nS2FIbXUyeWhsLTFXaW1pQkFSMjRvSnEwcFFVdG56TDZrbEZCNVhlRTlMMjZ6U0hSQnFaNDdUY1N3blZZSlRhR0pJNXJWRkhfdS1DVEgwbmFqUnNSdXB3ajBFS3hXUzM0U0d4NldpaHF4d0NkQXM3VlVoblgwUTdraTFVSzBESTFyMS1PR2VrMXZWbmt0WTNtekNPVlNJMXBUVkZNNVE1Vy1kVDN6bkdYUkFEWm5ObkpjcTFWR3NsUWF0SUZMX0ZvdnAwZlBDZnN4RnhYQ3NCZUU2LUNzM1NSU1NxMzB3V0RIY3ZNQXFzVXo4Q2JQVTFST1VCVlFJa2xxMlpqZnVZV2pvZzIxZ2REU1BhRkNYRWVzd2hoUVdTZG1Ud1c1MTV0NTA1TFNoZjRma19mcEtNRjNpTGNLaWZkVjVlM3RSZyJ9.W4y8flAWh6bqSDnDcM7HZUztzma66j2IJeQNzKWSBi7auH7PG85_x-aLNk6kYX-X-GIZG26iBIa-yP7GiWvwrAEm--hYzWTpRl6e0NGG3yFA42LbR-HzaXIenv_FKbh7YoLujoltDA1lYje8PJZvH5DRfga8MVLMiVBSwNJ5iY7DrNUZmOVTg6Cfr-HPWksxCJvT6TeTihxhK6u1cqBV2WiY7YSo9__YAsjN9O42bsBeN-MMqJU0J7OOdMyOueoR1H_rjbNGYg5-wkKMEYDTIkIkNCbHKPlgONzcmFhzqs1dO9eKNbA140NtMXrM9rL_dqQMzwcpL7_sVxaeGF2IlQ

this is the tool i whipped up to generate the above token.

luckily it looks like NSS didn't mind that the .well-known/openid-configuration was application/octet-stream instead of application/json. :) nginx doesn't have a way to set a content-type on a file without an extension (without setting the default content-type for everything in a directory).

i plan to do some more checking before i open a bug against NSS, to make sure i'm not doing something wrong.

zenomt commented 5 years ago

from source code inspection, i've found at least one bug in NSS (oidc-auth-manager) and one misfeature.

bug: oidc-auth-manager https://github.com/solid/oidc-auth-manager/blob/5827449e0d1d287668fff667661003e529eb8f9d/src/preferred-provider.js#L70-L73 drops any path part of an OIDC issuer URI, so it can never successfully match an id_token's iss if that claim has a path part (as my regular webid does, as well as the bot example above). issuers are allowed to have path parts, see (among other references) https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest .

misfeature: only one solid:oidcIssuer is properly supported. if a profile lists more than one, the results are nondeterministic: https://github.com/solid/oidc-auth-manager/blob/5827449e0d1d287668fff667661003e529eb8f9d/src/preferred-provider.js#L105 . i had hoped this wasn't the case at https://github.com/solid/webid-oidc-spec/issues/30#issuecomment-497854769 .

i'll open a bug and enhancement for these two against oidc-auth-manager this weekend.

just from code inspection i still don't know why it won't load my profile document after doing an OPTIONS on it.

zenomt commented 5 years ago

i plan to do some more checking before i open a bug against NSS, to make sure i'm not doing something wrong.

the tool i wrote can generate a usable access token if the issuer URI has no path part.

as another annoyance: NSS seems to cache some representations (like for the openid-configuration and jwks_uri) for way too long, and appears to ignore the Cache-control header. i have "Cache-control: max-age=60" on responses for those but NSS doesn't seem to reload them even after a day+.