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 Data Pods (resource servers).
2. Definitions
- OpenID Provider (OP)
- An OAuth 2.0 authorization server implementing OpenID Connect as defined in the OpenID Connect Core 1.0 specification. [OpenIDConnect]
- Relying Party (RP)
- 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]
- Resource
- Something denoted by an IRI or a literal as defined in RDF 1.1. [rdf11-concepts]
- Resource Owner
- 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]
- Resource Server (RS)
- 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]
- Solid Data Pod (Pod)
- A Solid compliant Resource Server as defined in the Solid Specification. [Solid]
- Solid OpenID Connect (Solid OIDC)
- The specification defining authentication in the Solid ecosystem. [SolidOIDC]
- WebID
- 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
- 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 OP
- 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 Pod, for example Alice’s and Bob’s). In our example, Alice’s
OP is hosted at
https://secureauth.example
. - Alice’s WebID
- Alice’s WebID is
https://alice.coolpod.example/profile/card#me
. The WebID profile document denoted by URIhttps://alice.coolpod.example/profile/card
is hosted on Alice’s Solid Data Pod and contains the URI of her OP. Alice’s WebIDhttps://alice.coolpod.example/profile/card#me
serves as her unique identifier in the Solid Ecosystem. - RP
- 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
. - RP’s WebID
- Decent Photos is a Solid compliant application and has a WebID of its own. An
RP’s WebID contains information identifying them as a registered OAuth 2.0 client
application. Decent Photo’s WebID is
https://decentphotos.example/webid#this
- Bob’s Pod
- We will be trying to access photos stored in Bob’s Pod. 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 Data Pod is hosted at
https://bob.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
)
While it is not the case with Alice, a user’s Pod and OP can be hosted at the same domain. For
example, Bob’s Pod could be bob.otherpod.example
with a WebID of https://bob.otherpod.example/profile/card#me
, but his OP is at https://solid.example
.
2.1 Retrieve Profile (only needed if a WebID is provided)
If Alice entered 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 hostname 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" , "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 [recommends
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
grand 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%20profile%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%20profile%20offline_access
: a list of OIDC scopes (attributes of the RS to which this token should have access) separated by spaces (%20).-
open_id
is a scope that is needed to verify Alice’s identity. -
profile
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 WebID (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 WebID, you can either register an app using static registration
via some UI on the OP or dynamic registration.
You may also use http://www.w3.org/ns/solid/terms#PublicOidcClient
as your client id. This will designate the
client as a public client. Doing this may cause the RS to provide less access to your app.
7. Fetch RP WebID
If an app WebID is provided as the client id (see note above to see other options), we must fetch that app WebID to confirm its validity.
For the WebID https://decentphotos.example/webid#this
, request the WebID Profile with:
GET https://decentphotos.example/webid
Response:
@prefix solid : .< #this > solid :oidcRegistration """ { " client_id " :" https://decentphtos.example/webid#this " ," redirect_uris " : [" https://decentphotos.example/callback " ]," client_name " :" DecentPhotos " ," client_uri " :" https://decentphotos.example/ " ," logo_uri " :" https://decentphotos.example/logo.png " ," tos_uri " :" https://decentphotos.example/tos.html " ," scope " :" openid profile offline_access " ," grant_types " : [" refresh_token " ," authorization_code " ]," response_types " : [" code " ]," default_max_age " :60000 ," require_auth_time " :true }""" .
Notice that the application WebID contains one required triple ?webid solid:oidcRegistration
jsonRegistration
. This is a JSON representation of an OIDC Client Registration.
8. Validate redirect url with WebID
Check to be sure that the redierct_uri
value provided in the auth request
(https://decentphotos.example/callback
) is listed in the redirect_uris
array in the
oidcRegistration. If it is not, the OP MUST reject the request with a 403
. In our case, it
is, 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" , "profile" , "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 Pods while ensuring that evil Pods can’t steal 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 storage -
body.code=m-OrTPHdRsm8W_e9P0J2Bt
: The code that we received from the OP upon redirect -
body.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 403.
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 403.
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 403.
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
18. Generates access token
The OP generates an access token. To do so it constructs a JSON Web Token and signs it using its own keys. The access token could look like (remember you can decode it using https://jwt.io):
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL2FsaWNlLmNvb2xwb2QuZXhhbXBsZS9wcm9maWxlL2NhcmQjbWUiLCJ3ZWJpZCI6Imh0dHBzOi8vYWxpY2UuY29vbHBvZC5leGFtcGxlL3Byb2ZpbGUvY2FyZCNtZSIsImlzcyI6Imh0dHBzOi8vc2VjdXJlYXV0aC5leGFtcGxlIiwiYXVkIjoic29saWQiLCJjbmYiOnsiamt0IjoiOVhtd0s4bVEzSDUtUG56QXQzbEZIeldCV192NVFoWXluZXpiYml0NGtDOCJ9LCJjbGllbnRfaWQiOiJodHRwczovL2RlY2VudHBob3Rvcy5leGFtcGxlL3dlYmlkI3RoaXMiLCJqdGkiOiJmZDBhMTM1My0yNWYzLTRjN2UtOGY4Yi1jMTQyNjRmMWMxMmUiLCJpYXQiOjE2MDMzODUyNjEsImV4cCI6MTYwMzM4NTg2MX0.HIIYiovILPvsdkD3s3xomR1MkA_Ir8Mx_C-eHxbWEnw9Z2rv2rOWbvMPZ--BqH3qVSupgYTZZsTDbZXA8giCLA
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:
{ "webid" : "https://alice.coolpod.example/profile/card#me" , "iss" : "https://secureauth.example" , "aud" : "solid" , "cnf" : { "jkt" : "9XmwK8mQ3H5-PnzAt3lFHzWBW_v5QhYynezbbit4kC8" }, "client_id" : "https://decentphotos.example/webid#this" , "jti" : "fd0a1353-25f3-4c7e-8f8b-c14264f1c12e" , "iat" : 1603385261 , "exp" : 1603385861 }
-
"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 -
"aud": "solid"
: The token’s audience. Because DPoP handles scoping the audience in all practical senses, the audience for a solid token is simply "solid." -
"cnf": { "jkt": "9XmwK8mQ3H5-PnzAt3lFHzWBW_v5QhYynezbbit4kC8" }
: The jwk thrumbprint must be embedded in an object on the field "jkt" -
"client_id": "https://decentphotos.example/webid#this"
: The client id of the application -
"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
19. Generates the id_token
If openid
was listed as a scope during the authorization request, the OP generates an id
token. The id token is not used to access anything. It is simply to communicate information to
the client. This token is a JSON Web Token. It would look like
the following (you can decrypt the token with https://jwt.io):
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" : "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 }
-
"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, its audience 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
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" : "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL2FsaWNlLmNvb2xwb2QuZXhhbXBsZS9wcm9maWxlL2NhcmQjbWUiLCJ3ZWJpZCI6Imh0dHBzOi8vYWxpY2UuY29vbHBvZC5leGFtcGxlL3Byb2ZpbGUvY2FyZCNtZSIsImlzcyI6Imh0dHBzOi8vc2VjdXJlYXV0aC5leGFtcGxlIiwiYXVkIjoic29saWQiLCJjbmYiOnsiamt0IjoiOVhtd0s4bVEzSDUtUG56QXQzbEZIeldCV192NVFoWXluZXpiYml0NGtDOCJ9LCJjbGllbnRfaWQiOiJodHRwczovL2RlY2VudHBob3Rvcy5leGFtcGxlL3dlYmlkI3RoaXMiLCJqdGkiOiJmZDBhMTM1My0yNWYzLTRjN2UtOGY4Yi1jMTQyNjRmMWMxMmUiLCJpYXQiOjE2MDMzODUyNjEsImV4cCI6MTYwMzM4NTg2MX0.HIIYiovILPvsdkD3s3xomR1MkA_Ir8Mx_C-eHxbWEnw9Z2rv2rOWbvMPZ--BqH3qVSupgYTZZsTDbZXA8giCLA" , "expires_in" : 300 , "token_type" : "DPoP" , "id_token" : "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL2FsaWNlLmNvb2xwb2QuZXhhbXBsZS9wcm9maWxlL2NhcmQjbWUiLCJhdWQiOiJodHRwczovL2RlY2VudHBob3Rvcy5leGFtcGxlL3dlYmlkI3RoaXMiLCJ3ZWJpZCI6Imh0dHBzOi8vYWxpY2UuY29vbHBvZC5leGFtcGxlL3Byb2ZpbGUvY2FyZCNtZSIsImlzcyI6Imh0dHBzOi8vc2VjdXJlYXV0aC5leGFtcGxlIiwianRpIjoiODQ0YTA5NWMtOWNkYi00N2U1LTk1MTAtMWRiYTk4N2MwYTVmIiwiaWF0IjoxNjAzMzg2NDQ4LCJleHAiOjE2MDMzODcwNDh9.T306vT8dmn9gQIMEdG92AM4WRnrhqWZTfDpovwqZ6Zn0mK9yxj0iOVGqXD4CW8-tzDTitNwEGorAo85atL0Oeg" , "refresh_token" : "eyJhbGciOiJub25lIn0.eyJqdGkiOiJhNzhiNDllZi03MWM1LTQ5ODUtYTUwYy01ZWYzYWVmMGZkOGYifQ." , "scope" : "openid profile offline_access" }
-
access_token": "eyJhbGciOiJ..."
: The access token we generated. The client will use this to authenticate with the server. -
"expires_in": 300
: Tells the client that the access token will expire in 300 seconds (5 minutes) -
"token_type": "DPoP"
: Tells the client that the token type is DPoP -
"id_token": "eyJhbGciOiJFU..."
: The id token we generated. The client will use this to extract information like the user’s WebId. -
"refresh_token": "eyJhbGciOiJ..."
: The refresh token. The client will use this to get a new access token when its current one expires. -
"scope": "openid profile offline_access"
: The scopes that were used.
4.2. Request Flow
1. An AJAX request is initiated
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 Pod 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.
2. Creates a DPoP header token
Before we send a request, 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://bob.otherpod.example/private/photo_album.ttl" , "htm" : "GET" , "jti" : "fb1264dd-fff1-4509-a7b1-0fe58d08d3e1" , "iat" : 1603389616 }
-
"htu": "https://bob.otherpod.example/private/photo_album.ttl"
: htu limits the token for use only on the given url. -
"htm": "GET"
: 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.
3. Sends request
With the access token and new DPoP token in hand, we’re ready to make our request.
GET https://bob.otherpod.example/private/photo_album.ttl Headers: { authorization: "DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL2FsaWNlLmNvb2xwb2QuZXhhbXBsZS9wcm9maWxlL2NhcmQjbWUiLCJ3ZWJpZCI6Imh0dHBzOi8vYWxpY2UuY29vbHBvZC5leGFtcGxlL3Byb2ZpbGUvY2FyZCNtZSIsImlzcyI6Imh0dHBzOi8vc2VjdXJlYXV0aC5leGFtcGxlIiwiYXVkIjoic29saWQiLCJjbmYiOnsiamt0IjoiOVhtd0s4bVEzSDUtUG56QXQzbEZIeldCV192NVFoWXluZXpiYml0NGtDOCJ9LCJjbGllbnRfaWQiOiJodHRwczovL2RlY2VudHBob3Rvcy5leGFtcGxlL3dlYmlkI3RoaXMiLCJqdGkiOiJmZDBhMTM1My0yNWYzLTRjN2UtOGY4Yi1jMTQyNjRmMWMxMmUiLCJpYXQiOjE2MDMzODUyNjEsImV4cCI6MTYwMzM4NTg2MX0.HIIYiovILPvsdkD3s3xomR1MkA_Ir8Mx_C-eHxbWEnw9Z2rv2rOWbvMPZ--BqH3qVSupgYTZZsTDbZXA8giCLA", dpop: "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwia2lkIjoiQ21HVE9Dd3ZKWXhrb0dGOGNxcFpBNTdab2xVdThBcFJQb3MwVlduWk1TNCIsInVzZSI6InNpZyIsImFsZyI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiU0FZcmF5VUh4Z1FPQ29YSC1MbHdyOW1iSWJpUHBsLXRQRUpLeE1WWFltcyIsInkiOiJ6eGJQODdPQ3JpeEZpMk9vZjU1QkhsTC1ySHhvWHVuUmttNFBkV3duUzJnIn19.eyJodHUiOiJodHRwczovL2JvYi5vdGhlcnBvZC5leGFtcGxlL3ByaXZhdGUvcGhvdG9fYWxidW0udHRsIiwiaHRtIjoiZ2V0IiwianRpIjoiZmIxMjY0ZGQtZmZmMS00NTA5LWE3YjEtMGZlNThkMDhkM2UxIiwiaWF0IjoxNjAzMzg5NjE2LCJleHAiOjE2MDMzOTMyMTZ9.G8JktoMOadenCYtO4Z_ZI7ACnjKJvT59OyKlQ6WpB1Qq2GoCK6v1ocrpsfELDOKIL5nt5fwWccfvCAA2bMrkjA" }
-
headers.authorization: "DPoP eyJhbGciOiJFUzI1N..."
: The authorization header contains the ACCESS TOKEN that we received from the OP during the auth phase. Putting "DPoP" followed by a space tells the Resource Server that this is a DPoP bound access token. -
dpop: "eyJhbGciOi..."
: The DPoP token we just generated.
4. Checks Access Token expirations
The RS checks if the access 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 RS must reject the request with a 403.
5. Checks the DPoP token url and method
The RS 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 RS must reject the request with a 403.
5.1. (Optional) Checks DPoP token unique identifier
The RS 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
RS receives a DPoP token with a jti
it has already encountered it may reject the request with
a 403.
6. Checks DPoP signature against Access Token
The RS first confirms that the DPoP token was signed by the public key listed in its header
jwk
. If it was not, the RS must reject the request with a 403.
The RS 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 with a 403.
7. Retrieves Profile
Using the webid
claim in the access token (for us, it’s https://alice.coolpod.example/profile/card#me
), the RS 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 > ;
8. Checks issuer
The RS checks that the iss
claim in the access token matches the issuer listed in the user’s
WebID. If it does not, the RS must reject the request with a 403.
9. Retrieves OP configuration
The RS 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" , "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 }
10. Requests JWKS
Using the jwks_uri
field in the openid-configuration, the RS 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.
11. Checks access token signature validity
Using the kid
value in the access token, the RS searches the OP keys for one that matches the
access token. Checks to see if the access token was signed by that key. If it was not, the RS
must reject the request with a 403.
12. Performs Authorization
Now that the RS 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 RS performs authorization.
13. Returns Result
Given all went well, the RS should return the requested content.