Solid-OIDC Primer (original) (raw)
1. Introduction
This document outlines in details how Alice (end-user) asserts her identity (logs in) when using Decent Photos (relying party) to access data in hers and Bob’s Solid Storage (resource servers).
2. Definitions
An OAuth 2.0 authorization server implementing OpenID Connect as defined in the OpenID Connect Core 1.0 specification. [OIDC.Core]
A client application using OpenID Connect to make resource requests on behalf of the resource owner. Client is one of the four roles defined in the OAuth 2.0 specification. [RFC6749]
Something denoted by an IRI or a literal as defined in RDF 1.1. [rdf11-concepts]
An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user. Resource Owner is one of the four roles defined in the OAuth 2.0 specification. [RFC6749]
A server hosting resources, capable of accepting and responding to protected resource requests using access tokens. RS is one of the four roles defined in the OAuth 2.0 specification. [RFC6749]
A Solid compliant Resource Server as defined in the Solid Protocol. [Solid.Protocol]
Solid OpenID Connect (Solid OIDC)
The specification defining authentication in the Solid ecosystem. [Solid.OIDC]
A WebID is an HTTP URI which refers to an Agent (Person, Organization, Group, Device, etc.) as defined in the WebID 1.0 specification. [WebID]
3. Actors
Several actors are at play in our example Solid OIDC authentication flows:
Alice will be providing consent for Decent Photos to make resource requests on her behalf. Let’s assume that Alice is using a standard web browser.
Alice’s OpenID Provider, also known as an Identity Provider (IdP), is the service responsible for authorizing our third-party web app (Decent Photos) by providing it with the tokens necessary to gain access to any resource Alice has access to (in any Storage, for example Alice’s and Bob’s). In our example, Alice’s OP is hosted at https://secureauth.example
.
Alice’s WebID is https://alice.coolpod.example/profile/card#me
. The WebID profile document denoted by URI https://alice.coolpod.example/profile/card
is hosted on Alice’s Solid Storage and contains the URI of her OP. Alice’s WebID https://alice.coolpod.example/profile/card#me
serves as her unique identifier in the Solid Ecosystem.
The Decent Photos application is a third party photo viewing web application. It is OIDC compliant and will therefore be referred to as the Relying Party. The Decent Photos web app allows its users to view photos they have access to. For example, Alice’s and her friend Bob’s photos. In our example, Decent Photos is hosted at https://decentphotos.example
.
Decent Photos is a Solid compliant application and has a URI of its own, which resolves to a Client ID Document. An RP’s Client ID Document contains information identifying them as a registered OAuth 2.0 client application. Decent Photo’s URI is https://decentphotos.example/webid#this
We will be trying to access photos stored in Bob’s Storage. In our example, Bob is a friend of Alice and has previously indicated via access control that Alice may access some of his photos using any web app. Bob’s Solid Storage is hosted at https://bob.otherpod.example
.
Authorization Server for Bob’s Storage
The Authorization Server (AS) connected to Bob’s Storage will issue access tokens that can be used with Bob’s Storage. In these examples, this is hosted at https://auth.otherpod.example
.
4. Solid OIDC Flow
4.1. Authorization Code Grant with PKCE Authorization Flow
The Authorization Code grant with PKCE is the primary OAuth 2.0 authorization flow recommended by the Solid OIDC. It is defined by the PKCE RFC [RFC7636] and described here in the Solid OIDC context.
1. Alice uses the Decent Photos web app
Alice has heard of a great new site that allows her to view her friend’s photos and tag faces. She navigates to https://decentphotos.example
via her web browser which returns an HTML page. This page contains JavaScript that will help with the authentication process.
2. Alice selects her OP or WebID
Before decentphotos can start displaying images, Alice needs to start the process of providing consent. To do so, she must either provide her WebID (https://alice.coolpod.example/profile/card#me
) or the URL of her OP (https://secureauth.example
).
Although it is not the case for Alice, a user’s Storage and OP can be hosted under the same domain. For example, Bob could have a Storage at https://bob.solidpod.example.com
, an OP at https://solidpod.example.com/
, and a WebID of https://bob.solidpod.example.com/profile/card#me
.
2.1 Retrieve Profile (only needed if a WebID is provided)
If Alice enters her WebID’s URL rather than her OP’s URL, a request should be made to determine her OP. To do so, a request should be made to Alice’s WebId:
GET https://alice.coolpod.example/profile/card#me
It will return a body similar to this:
@prefix : <#>. @prefix solid: http://www.w3.org/ns/solid/terms#. @prefix foaf: http://xmlns.com/foaf/0.1/. @prefix schema: http://schema.org/.
<> a foaf:PersonalProfileDocument ; foaf:maker https://localhost:8443/profile/card#me ; foaf:primaryTopic https://localhost:8443/profile/card#me .
:me a foaf:Person ; a schema:Person ; foaf:name "Alice" ; solid:oidcIssuer https://secureauth.example ;
The OP URL is located at :me -> solid:oidcIssuer
3. Retrieves OP Configuration
Now that we have Alice’s OP’s URL, the RP must make a request to retrieve the OP’s configuration. This is always located at the OP’s issuer URL followed by /.well-known/openid-configuration
.
GET https://secureauth.example/.well-known/openid-configuration
The openid-configuration describes everything the client will need to know to authorize with Alice’s specific OP.
Response Body:
{ "issuer": "https://secureauth.example", "authorization_endpoint": "https://secureauth.example/authorize", "token_endpoint": "https://secureauth.example/token", "userinfo_endpoint": "https://secureauth.example/userinfo", "registration_endpoint": "https://secureauth.example/register", "end_session_endpoint": "https://secureauth.example/endsession", "jwks_uri": "https://secureauth.example/jwks", "response_types_supported": [ "code" ], "grant_types_supported": [ "authorization_code", "refresh_token" ], "subject_types_supported": [ "public" ], "claims_supported": [ "sub", "webid" ], "scopes_supported": [ "openid", "webid", "profile", "email", "offline_access" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic" ], "token_endpoint_auth_signing_alg_values_supported": [ "ES256" ], "request_object_signing_alg_values_supported": [ "ES256" ], "id_token_signing_alg_values_supported": [ "ES256" ], "code_challenge_methods_supported": [ "plain", "S256" ], "claims_parameter_supported": false, "request_parameter_supported": true, "request_uri_parameter_supported": false, "require_request_uri_registration": false }
The thing we care about here is the authorization_endpoint
field. This will be the url we use when we’re ready to send an authorization request to the OP.
Note that we only support the code
response type. The OIDC discovery specification states that Dynamic OpenID Providers MUST also support id_token
and token id_token
. However, implicit flows should not be used for security reasons. Therefore, we don’t recommend enabling them. For the same reason, we do not support the implicit
grant type.
4. Generates PKCE code challenge and code verifier
To follow the PKCE code flow we need to generate a code challenge and code verifier as instructed in the Proof Key for Code Exchange spec.
We start by generating a code verifier. The can be done by creating a cryptographically random string. Let’s say ours looks like this: "JXPOuToEB7"
.
Now using the code verifier, we construct a code challenge. This can be made by transforming the code verifier with Sha 256: BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
. Here’s our code challenge: "HSi9dwlvRpNHCDm-L8GOdM16qcb0tLHPZqQSvaWXTI0"
5. Saved code verifier to session storage
Now, we save the code verifier ("JXPOuToEB7"
) to session storage. We’ll need it later to confirm to the OP that this is the app that initiated the request. Do not save the code challenge anywhere.
6. Authorization request
Now that the web app is registered, we can finally make an auth request to authorize the web application.
GET https://secureauth.example/authorize?response_type=code& redirect_uri=https%3A%2F%2Fdecentphotos.example%2Fcallback& scope=openid%20webid%20offline_access& client_id=https%3A%2F%2Fdecentphotos.example%2Fwebid%23this& code_challenge_method=S256& code_challenge=HSi9dwlvRpNHCDm-L8GOdM16qcb0tLHPZqQSvaWXTI0
That URL might look a little complex, but it’s essentially a request to https://secureauth.example/authorize
with the following URL parameters:
response_type=code
indicates the desired response data.redirect_uri=https%3A%2F%2Fdecentphotos.example%2Fcallback
: The url that the OP will redirect to once the user has logged in (https://decentphotos.example/callback
).scope=openid%20webid%20offline_access
: a list of OIDC scopes (attributes of the RS to which this token should have access) separated by spaces (%20).openid
is a scope that is needed to verify Alice’s identity.webid
is required by the Solid OIDC specification to denote a WebID login.offline_access
: Is required to get a refresh token.
client_id=https%3A%2F%2Fdecentphotos.example%2Fwebid%23this
: Usually the client id of a Solid application is the app’s URI (in our casehttps://decentphotos.example/webid#this
) as seen here.code_challenge_method=S256
: Tells the OP that our code challenge was transformed using SHA-256.code_challenge=HSi9dwlvRpNHCDm-L8GOdM16qcb0tLHPZqQSvaWXTI0
: The code challenge we generated before.
Note: If the app doesn’t have a URI, you can either register an app using static registration via some UI on the OP or use dynamic registration.
7. Fetch RP Client ID Document
If an app URI is provided as the client id (see note above to see other options), we must fetch that app URI to confirm its validity.
For the URI https://decentphotos.example/webid#this
, request the Client ID Document with:
GET https://decentphotos.example/webid
Response:
{ "@context": [ "https://www.w3.org/ns/solid/oidc-context.jsonld" ],
"client_id": "https://decentphtos.example/webid#this", "client_name": "DecentPhotos", "redirect_uris": [ "https://decentphotos.example/callback" ], "post_logout_redirect_uris": [ "https://decentphotos.example/logout" ], "client_uri": "https://decentphotos.example/", "logo_uri": "https://decentphotos.example/logo.png", "tos_uri": "https://decentphotos.example/tos.html", "scope": "openid webid offline_access", "grant_types": [ "refresh_token", "authorization_code" ], "response_types": [ "code" ], "default_max_age": 3600, "require_auth_time": true }
Notice that the application Client ID Document contains a JSON-LD representation of an OIDC Client Registration. It also must use the specific "@context": ["https://www.w3.org/ns/solid/oidc-context.jsonld"]
.
8. Validate redirect url with Client ID Document
Check to be sure that the redirect_uri
value provided in the auth request (https://decentphotos.example/callback
) is listed in the redirect_uris
array in the Client ID Document. If it is not, the OP must reject the request. In our case, the redirect_uri
is valid, so we may continue.
9. Alice Logs In
The OP should redirect to its login screen. The actual implementation of this is completely up to the OP. A user can log in with her password, a TLS certificate, or any other proven method of authentication. The important thing is that, thanks to the redirect, the control is now out of the hands of the RP and is in complete control of the OP.
10. Generate a code
Generate a cryptographically random string that will be used as a code (Let’s say ours is m-OrTPHdRsm8W_e9P0J2Bt
). Store that string in a persistant keystore as the key for the client id, the code challenge, the user’s webid, their desired response types, and their scopes:
{ "m-OrTPHdRsm8W_e9P0J2Bt": { "client_id": "https://decentphotos.example/webid#this", "code_challenge": "HSi9dwlvRpNHCDm-L8GOdM16qcb0tLHPZqQSvaWXTI0", "webid": "https://alice.coolpod.example/profile/card#me", "response_types": [ "code" ], "scope": [ "openid", "webid", "offline_access" ] } }
11. Send code to redirect url
Once Alice successfully logs in, the OP redirects back to the application via the provided redirect uri, including useful information with it:
302 redirect to: https://decentphotos.example/callback?code=m-OrTPHdRsm8W_e9P0J2Bt
This redirect gives decentphotos the code that it will exchange for an access token.
12. Generates a DPoP Client Key Pair
Solid-OIDC depends on Demonstration of Proof-of-Possession (DPoP) tokens. DPoP tokens ensure that third-party web applications can send requests to any number of Storage servers while ensuring that malicious actors can’t steal and replay a user’s token.
The first step to generating a DPoP token is generating a public and private key pair on the third-party RP. In our example, the private key is generated using elliptic curves and looks like:
{ "kty": "EC", "kid": "2i00gHnREsMhD5WqsABPSaqEjLC5MS-E98ykd-qtF1I", "use": "sig", "alg": "EC", "crv": "P-256", "x": "N6VsICiPA1ciAA82Jhv7ykkPL9B0ippUjmla8Snr4HY", "y": "ay9qDOrFGdGe_3hAivW5HnqHYdnYUkXJJevHOBU4z5s", "d": "RrM4Ou_7PzjP24B4k06B9ZML16HbfzNPKFN11Z8c9_s" }
From now on we will refer to this as RP_PRIVATE_KEY
.
The public key looks like:
{ "kty": "EC", "kid": "2i00gHnREsMhD5WqsABPSaqEjLC5MS-E98ykd-qtF1I", "use": "sig", "alg": "EC", "crv": "P-256", "x": "N6VsICiPA1ciAA82Jhv7ykkPL9B0ippUjmla8Snr4HY", "y": "ay9qDOrFGdGe_3hAivW5HnqHYdnYUkXJJevHOBU4z5s" }
13. Generates a DPoP Header
Now that we generated a private key for the client, we need to generate the DPoP header. To do so, we create a JSON Web Token and sign it using the key we generated.
Our token could look like the following (you can decode the token using https://jwt.io):
eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwia2lkIjoiZkJ1STExTkdGbTQ4Vlp6RzNGMjVDOVJmMXYtaGdEakVnV2pEQ1BrdV9pVSIsInVzZSI6InNpZyIsImFsZyI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiOWxlT2gxeF9IWkhzVkNScDcyQzVpR01jek1nUnpDUFBjNjBoWldfSFlLMCIsInkiOiJqOVVYcnRjUzRLVzBIYmVteW1vRWlMXzZ1cko0TFFHZXJQZXVNaFNEaV80In19.eyJodHUiOiJodHRwczovL3NlY3VyZWF1dGguZXhhbXBsZS90b2tlbiIsImh0bSI6InBvc3QiLCJqdGkiOiI0YmEzZTllZi1lOThkLTQ2NDQtOTg3OC03MTYwZmE3ZDNlYjgiLCJpYXQiOjE2MDMzMDYxMjgsImV4cCI6MTYwMzMwOTcyOH0.2lbgLoRCkj0MsDc9BpquoaYuq0-XwRf_URdXru2JKrVzaWUqQfyKRK76_sQ0aJyVwavM3pPswLlHq2r9032O7Q
Token Header:
{ "alg": "ES256", "typ": "dpop+jwt", "jwk": { "kty": "EC", "kid": "2i00gHnREsMhD5WqsABPSaqEjLC5MS-E98ykd-qtF1I", "use": "sig", "alg": "EC", "crv": "P-256", "x": "N6VsICiPA1ciAA82Jhv7ykkPL9B0ippUjmla8Snr4HY", "y": "ay9qDOrFGdGe_3hAivW5HnqHYdnYUkXJJevHOBU4z5s" } }
"alg": "ES256"
: The signing algorithm used in this token. In this case,ES256
because we generated the keys using eliptic curves."typ": "dpop+jwt"
: The type of token. All DPoP Tokens should have a type of "dpop+jwt""jwk": { "kty": "EC" ... }
: The client’s public key.
Token Body:
{ "htu": "https://secureauth.example/token", "htm": "POST", "jti": "4ba3e9ef-e98d-4644-9878-7160fa7d3eb8", "iat": 1603306128 }
"htu": "https://secureauth.example/token"
: htu limits the token for use only on the given url."htm": "POST"
: htm limits the token for use only on a specific http method, in this casePOST
."jti": "4ba3e9ef-e98d-4644-9878-7160fa7d3eb8"
: jti is a unique identifier for the DPoP token that can optionally be used by the server to defend against replay attacks"iat": 1603306128
: The date the token was issued, in this case October 21, 2020 15:52:33 GMT.
14. Token request with code and code verifier
Now, we have everything we need to make an auth request. No need to redirect the web browser for this one. We only need to make an AJAX request to the token
endpoint as defined in the OP’s openid-configuration
file, in our case https://secureauth.example/token
POST https://secureauth.example/token Headers: { "DPoP": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwia2lkIjoiZkJ1STExTkdGbTQ4Vlp6RzNGMjVDOVJmMXYtaGdEakVnV2pEQ1BrdV9pVSIsInVzZSI6InNpZyIsImFsZyI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiOWxlT2gxeF9IWkhzVkNScDcyQzVpR01jek1nUnpDUFBjNjBoWldfSFlLMCIsInkiOiJqOVVYcnRjUzRLVzBIYmVteW1vRWlMXzZ1cko0TFFHZXJQZXVNaFNEaV80In19.eyJodHUiOiJodHRwczovL3NlY3VyZWF1dGguZXhhbXBsZS90b2tlbiIsImh0bSI6InBvc3QiLCJqdGkiOiI0YmEzZTllZi1lOThkLTQ2NDQtOTg3OC03MTYwZmE3ZDNlYjgiLCJpYXQiOjE2MDMzMDYxMjgsImV4cCI6MTYwMzMwOTcyOH0.2lbgLoRCkj0MsDc9BpquoaYuq0-XwRf_URdXru2JKrVzaWUqQfyKRK76_sQ0aJyVwavM3pPswLlHq2r9032O7Q", "content-type": "application/x-www-form-urlencoded" } Body: grant_type=authorization_code& code_verifier=JXPOuToEB7& code=m-OrTPHdRsm8W_e9P0J2Bt& redirect_uri=https%3A%2F%2Fdecentphotos.example%2Fcallback& client_id=https%3A%2F%2Fdecentphotos.example%2Fwebid%23this
headers.DPoP: "eyJhbGciOiJFUz..."
: The DPoP token we generated before. This will tell the OP what the client’s public key is.headers.content-type: "application/x-www-form-urlencoded"
: Sets to body’s content type toapplication/x-www-form-urlencoded
. Some OPs will accept other content types likeapplication/json
but they all must access urlencoded content types, so it’s safest to use that.body.grant_type=authorization_code
: Tells the OP that we are doing the authorization code flow.body.code_verifier=JXPOuToEB7
: Our code verifier that we stored in session storagebody.code=m-OrTPHdRsm8W_e9P0J2Bt
: The code that we received from the OP upon redirectbody.redirect_uri
: The app’s redirect url. In this case, this isn’t needed because we’re doing an AJAX request.body.client_id=https%3A%2F%2Fdecentphotos.example%2Fwebid%23this
: The app’s client id.
Once this request is completed decentphotos can remove the code verifier from session storage.
15. Validate code verifier
The OP looks up the code that was saved earlier in a keystore. It checks to see that the client id in the keystore corresponds to the client id from the request. If it does not, it must reject the request with a 400 HTTP status and invalid_grant
error code.
The OP then verifies that the code verifier [corresponds with the code challenge] (https://tools.ietf.org/html/rfc7636#section-4.6) stored in the keystore.
BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
If they do not correspond the OP must reject the request with a 400 HTTP status and invalid_grant
error code.
16. Validates DPoP Token Signature
The OP extracts the client’s public key from the DPoP header (at header.jwk). It confirms that the DPoP token has a valid signature. If not, the OP must reject the request with a 400 HTTP status and invalid_dpop_proof
error code.
17. Converts the DPoP public key to a JWK thumbprint
Currently the DPoP token contains a JWK public key, but before we place it inside the access token, it needs to be converted into a JWK thumbprint. Our JWK thumbprint looks more like:
9XmwK8mQ3H5-PnzAt3lFHzWBW_v5QhYynezbbit4kC8
19. Generates the id_token
Since openid
was listed as a scope during the authorization request, the OP generates an id token. The id token will be pushed as claim to any Solid Authorization Server needed, which clients want to get Access Token from. This token is a JSON Web Token. It would look like the following (you can decrypt the token with https://jwt.io):
encoded token needs to be updated
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL2FsaWNlLmNvb2xwb2QuZXhhbXBsZS9wcm9maWxlL2NhcmQjbWUiLCJhdWQiOiJodHRwczovL2RlY2VudHBob3Rvcy5leGFtcGxlL3dlYmlkI3RoaXMiLCJ3ZWJpZCI6Imh0dHBzOi8vYWxpY2UuY29vbHBvZC5leGFtcGxlL3Byb2ZpbGUvY2FyZCNtZSIsImlzcyI6Imh0dHBzOi8vc2VjdXJlYXV0aC5leGFtcGxlIiwianRpIjoiODQ0YTA5NWMtOWNkYi00N2U1LTk1MTAtMWRiYTk4N2MwYTVmIiwiaWF0IjoxNjAzMzg2NDQ4LCJleHAiOjE2MDMzODcwNDh9.T306vT8dmn9gQIMEdG92AM4WRnrhqWZTfDpovwqZ6Zn0mK9yxj0iOVGqXD4CW8-tzDTitNwEGorAo85atL0Oeg
Token Header:
{ "alg": "ES256", "typ": "JWT" }
"alg": "ES256"
: indicates the token was signed using eliptic curve"typ": "JWT"
: indicates that this is a JSON web token
Token Body:
{ "sub": "https://alice.coolpod.example/profile/card#me", "aud": [ "solid", "https://decentphotos.example/webid#this"], "azp": "https://decentphotos.example/webid#this" "webid": "https://alice.coolpod.example/profile/card#me", "iss": "https://secureauth.example", "jti": "844a095c-9cdb-47e5-9510-1dba987c0a5f", "iat": 1603370641, "exp": 1603371241, "cnf": { "jkt": "9XmwK8mQ3H5-PnzAt3lFHzWBW_v5QhYynezbbit4kC8" }, }
"sub": "https://alice.coolpod.example/profile/card#me"
: The subject claim. It must be the same as the authenticated user’s WebID."aud": "https://decentphotos.example/webid#this"
: The token’s audience. Because an id_token is intended for the client and any Solid Authorization Server, its audience is the client id and the string "solid"."azp": "https://decentphotos.example/webid#this"
: The token’s authorized party. Because an id_token is intended to be used by the client, its authorized party is the client id."webid": "https://alice.coolpod.example/profile/card#me"
: The WebID of the user that logged in"iss": "https://secureauth.example"
: The OP that was used to generate this token"jti": "844a095c-9cdb-47e5-9510-1dba987c0a5f"
: The jti is an optional unique identifier for this token that can be used to prevent replay attacks."iat": 1603370641
: The date the token was issued, in this case October 22, 2020 8:44:01"exp": 1603371241
: The token’s expiration date, in this case October 22, 2020 8:54:01"cnf": { "jkt": "9XmwK8mQ3H5-PnzAt3lFHzWBW_v5QhYynezbbit4kC8" }
: The jwk thrumbprint must be embedded in an object on the field "jkt"
20. Generates refresh token
If offline_access
was provided as a scope, the OP creates an opaque token as a refresh token. It could be like the one below. Notice the one below is a JWT, but a refresh token does not need to be a JWT.
eyJhbGciOiJub25lIn0.eyJqdGkiOiJhNzhiNDllZi03MWM1LTQ5ODUtYTUwYy01ZWYzYWVmMGZkOGYifQ.
The example token would decrypt as:
Token Header:
{ "alg": "none" }
Token Body:
{ "jti": "a78b49ef-71c5-4985-a50c-5ef3aef0fd8f" }
Save the refresh token to a persistant store.
21. Sends tokens
Once the OP has confirmed that everything checks out and all the tokens are generated, it sends a response with the tokens in the body:
Response (content-type: application/json)
{ "access_token": "531683bf-fea8-4bca-8976-2de50e5c9a50", "id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL2FsaWNlLmNvb2xwb2QuZXhhbXBsZS9wcm9maWxlL2NhcmQjbWUiLCJhdWQiOiJodHRwczovL2RlY2VudHBob3Rvcy5leGFtcGxlL3dlYmlkI3RoaXMiLCJ3ZWJpZCI6Imh0dHBzOi8vYWxpY2UuY29vbHBvZC5leGFtcGxlL3Byb2ZpbGUvY2FyZCNtZSIsImlzcyI6Imh0dHBzOi8vc2VjdXJlYXV0aC5leGFtcGxlIiwianRpIjoiODQ0YTA5NWMtOWNkYi00N2U1LTk1MTAtMWRiYTk4N2MwYTVmIiwiaWF0IjoxNjAzMzg2NDQ4LCJleHAiOjE2MDMzODcwNDh9.T306vT8dmn9gQIMEdG92AM4WRnrhqWZTfDpovwqZ6Zn0mK9yxj0iOVGqXD4CW8-tzDTitNwEGorAo85atL0Oeg", "refresh_token": "eyJhbGciOiJub25lIn0.eyJqdGkiOiJhNzhiNDllZi03MWM1LTQ5ODUtYTUwYy01ZWYzYWVmMGZkOGYifQ." }
"access_token": "531683bf-feea8..."
: The OAuth 2.0 access token, which may be used at the OP’suserinfo_endpoint
."id_token": "eyJhbGciOiJFU..."
: The ID token we generated. The client will use this to extract information such as the user’s WebId. A client will also exchange the ID token for an access token at an authorization server."refresh_token": "eyJhbGciOiJ..."
: The refresh token. The client will use this to fetch a new ID token when the current token expires.
4.2. Request Flow
In this example, Alice has logged into https://decentphotos.example
and has completed the authentication steps above. She wants to make a request to Bob’s Storage to get a photo album information at https://bob.otherpod.example/private/photo_album.ttl
. Bob has previously granted access to Alice but has not granted access to anyone else.
1. Discover Authorization Server
Client can discover Authorization Server by making request to the resource
Request:
GET https://bob.otherpod.example/private/photo_album.ttl
Response:
HTTP/1.1 401 Unauthorized WWW-Authenticate: UMA realm="example", as_uri="https://auth.otherpod.example", ticket="016f84e8-f9b9-11e0-bd6f-0021cc6004de"
2. Request AS configuration
Knowing Authorization Server now client can discover its Token Endpoint ([RFC8414])
Request:
GET https://auth.otherpod.example/.well-known/uma2-configuration
Response:
{ "issuer": "https://auth.otherpod.example", "token_endpoint": "https://auth.otherpod.example/token", "grant_types_supported": [ "urn:ietf:params:oauth:grant-type:uma-ticket" ] ... }
3. Creates a DPoP header token
Before we request access token, we need to generate a DPoP header token. A new DPoP token must be generated every time a request is made.
Generating a DPoP token is done the same way we did it in the authentication section. It must be signed by the same keypair that we generated in the authentication section. Our token could look like the following (you can decode the token using https://jwt.io):
eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwia2lkIjoiQ21HVE9Dd3ZKWXhrb0dGOGNxcFpBNTdab2xVdThBcFJQb3MwVlduWk1TNCIsInVzZSI6InNpZyIsImFsZyI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiU0FZcmF5VUh4Z1FPQ29YSC1MbHdyOW1iSWJpUHBsLXRQRUpLeE1WWFltcyIsInkiOiJ6eGJQODdPQ3JpeEZpMk9vZjU1QkhsTC1ySHhvWHVuUmttNFBkV3duUzJnIn19.eyJodHUiOiJodHRwczovL2JvYi5vdGhlcnBvZC5leGFtcGxlL3ByaXZhdGUvcGhvdG9fYWxidW0udHRsIiwiaHRtIjoiZ2V0IiwianRpIjoiZmIxMjY0ZGQtZmZmMS00NTA5LWE3YjEtMGZlNThkMDhkM2UxIiwiaWF0IjoxNjAzMzg5NjE2LCJleHAiOjE2MDMzOTMyMTZ9.G8JktoMOadenCYtO4Z_ZI7ACnjKJvT59OyKlQ6WpB1Qq2GoCK6v1ocrpsfELDOKIL5nt5fwWccfvCAA2bMrkjA
Token Header:
{ "alg": "ES256", "typ": "dpop+jwt", "jwk": { "kty": "EC", "kid": "2i00gHnREsMhD5WqsABPSaqEjLC5MS-E98ykd-qtF1I", "use": "sig", "alg": "EC", "crv": "P-256", "x": "N6VsICiPA1ciAA82Jhv7ykkPL9B0ippUjmla8Snr4HY", "y": "ay9qDOrFGdGe_3hAivW5HnqHYdnYUkXJJevHOBU4z5s", } }
"alg": "ES256"
: The signing algorithm used in this token. In this case,ES256
because we generated the keys using eliptic curves."typ": "dpop+jwt"
: The type of token. All DPoP Tokens should have a type of "dpop+jwt""jwk": { "kty": "EC" ... }
: The client’s public key.
Token Body:
{ "htu": "https://auth.otherpod.example/token", "htm": "POST", "jti": "fb1264dd-fff1-4509-a7b1-0fe58d08d3e1", "iat": 1603389616 }
"htu": "https://auth.otherpod.example/token"
: htu limits the token for use only on the given url."htm": "POST"
: htm limits the token for use only on a specific http method, in this casePOST
."jti": "fb1264dd-fff1-4509-a7b1-0fe58d08d3e1"
: jti is a unique identifier for the DPoP token that can optionally be used by the server to defend against replay attacks"iat": 1603389616
: The date the token was issued, in this case October 22, 2020 14:00:16 GMT.
4. Request Access Token
Request:
POST /token HTTP/1.1 Host: auth.otherpod.example DPoP: eyJhbGciOi... ... grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Auma-ticket &ticket=016f84e8-f9b9-11e0-bd6f-0021cc6004de &claim_token=eyj0... &claim_token_format=http%3A%2F%2Fopenid.net%2Fspecs%2Fopenid-connect-core-1_0.html%23IDToken
5. Checks ID Token expirations
The AS checks if the id token is still valid by looking at the exp
claim. If the token does not have an exp
claim or the token is expired, the AS must reject the request with a 403 HTTP status. When using UMA 2.0, the AS will include need_info
and required_claims
values in the error response.
6. Checks the DPoP token url and method
The AS checks the htu
and htm
claims of the DPoP token. If the htu
does not match the protocol, origin and path of the request or the htm
does not correspond the the http method of the request, the AS must reject the request with a 400 HTTP status and an invalid_dpop_proof
error code.
6.1. (Optional) Checks DPoP token unique identifier
The AS can optionally keep track of all DPoP jti
claims it received. Because a new DPoP token must be generated each time a request is made, no two tokens should have the same jti
. If the AS receives a DPoP token with a jti
it has already encountered it may reject the request with a 400 HTTP status and an invalid_dpop_proof
error code.
7. Checks DPoP signature against Access Token
The AS first confirms that the DPoP token was signed by the public key listed in its header jwk
. If it was not, the AS must reject the request with a 400 HTTP status and an invalid_dpop_proof
error code.
The AS checks if the public key in the DPoP token’s header.jwk
corresponds to the jwk thumbprint in the access token in the cnf
field. If they do not, the OP must reject the request. When using UMA 2.0, the response will include a 403 HTTP status code and a need_info
field in the error response.
8. Retrieves WebID Document
Using the webid
claim in the access token (for us, it’s https://alice.coolpod.example/profile/card#me
), the AS retreives the user’s WebId document.
Request
GET https://alice.coolpod.example/profile/card#me
Response
@prefix : <#>. @prefix solid: http://www.w3.org/ns/solid/terms#. @prefix foaf: http://xmlns.com/foaf/0.1/. @prefix schema: http://schema.org/.
<> a foaf:PersonalProfileDocument ; foaf:maker https://localhost:8443/profile/card#me ; foaf:primaryTopic https://localhost:8443/profile/card#me .
:me a foaf:Person ; a schema:Person ; foaf:name "Alice" ; solid:oidcIssuer https://secureauth.example ;
9. Checks issuer
The AS checks that the iss
claim in the ID token matches the issuer listed in the user’s WebID. If it does not, the AS must reject the request.
10. Retrieves OP configuration
The AS uses the iss
claim to get the issuer’s configuration. The url is the issuer claim with /.well-known/openid-configuration
appended to the end.
Request
GET https://secureauth.example/.well-known/openid-configuration
Response
{ "issuer": "https://secureauth.example", "authorization_endpoint": "https://secureauth.example/authorize", "token_endpoint": "https://secureauth.example/token", "userinfo_endpoint": "https://secureauth.example/userinfo", "registration_endpoint": "https://secureauth.example/register", "end_session_endpoint": "https://secureauth.example/endsession", "jwks_uri": "https://secureauth.example/jwks", "response_types_supported": [ "code" ], "grant_types_supported": [ "authorization_code", "refresh_token" ], "subject_types_supported": [ "public" ], "claims_supported": [ "sub", "webid" ], "scopes_supported": [ "openid", "webid", "profile", "email", "offline_access" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic" ], "token_endpoint_auth_signing_alg_values_supported": [ "ES256" ], "request_object_signing_alg_values_supported": [ "ES256" ], "id_token_signing_alg_values_supported": [ "ES256" ], "code_challenge_methods_supported": [ "plain", "S256" ], "claims_parameter_supported": false, "request_parameter_supported": true, "request_uri_parameter_supported": false, "require_request_uri_registration": false }
11. Requests JWKS
Using the jwks_uri
field in the openid-configuration, the AS makes a request to retreive the OP’s public keys.
Request
GET https://secureauth.example/jwks
Response (application/json)
{ "keys": [ { "alg": "EC", "crv": "P-256", "kid": "Xu68Q0ZfwDiRfWOb2UE8N77GoEQQ7q58_3gl1wsKENs", "kty": "EC", "use": "sig", "x": "qBbrIQNVTIm7M88iJFVB3e1GqsbFbYYfFiibkd48_Ac", "y": "SCl_hDR_6SEuJhKFOiVo8-zLqNglZ56jiJw2_PNE9hU" } ] }
Notice that the keys
field is an array, so an OP could have multiple public keys.
12. Checks ID token signature validity
Using the kid
value in the ID token, the AS searches the OP public keys for one that matches the one used to sign the ID token. If no public key is found, the AS must reject the request.
13. Access Token Response
Now that the AS has performed all of its checks, we can trust that the agent in the webid
claim (https://alice.coolpod.example/profile/card#me
) is the same agent on whose behalf the request was made. Using that information, the AS performs authorization and returns access token.
Response:
{ "access_token":"sbjsbhs(/SSJHBSUSSJHVhjsgvhsgvshgsv", "token_type":"Bearer" }
14. Sends request with Access Token
Note: In practice client would need to upgrade RPT by pushing authorization related claim. This primer only focuses on authentication so we omit this detail here.
With the access token, we’re ready to make our request.
GET https://bob.otherpod.example/private/photo_album.ttl Headers: { authorization: "Bearer sbjsbhs(/SSJHBSUSSJHVhjsgvhsgvshgsv", }
15. Returns Result
Given all went well, the RS should return the requested content.