Solid OIDC Primer

Editor’s Draft,

This version:
https://solid.github.io/authentication-panel/solid-oidc/primer.html
Latest published version:
https://solid.github.io/authentication-panel/solid-oidc/
Issue Tracking:
GitHub Issues
Editors:
Jackson Morgan
Aaron Coburn
Matthieu Bosquet

Abstract

The Solid OpenID Connect (Solid OIDC) specification defines how resource servers verify the identity of relying parties and end users based on the authentication performed by an OpenID provider. Solid OIDC builds on top of OpenID Connect 1.0. This primer is designed to provide the reader with the basic knowledge required to understand Solid OpenID Connect authentication flows. It introduces the basic concepts of authentication in the Solid ecosystem.

Status of this document

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 URI https://alice.coolpod.example/profile/card is hosted on Alice’s Solid Data Pod 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.
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).

Although it is not the case for Alice, a user’s Pod and OP can be hosted under the same domain. For example, Bob could have a Pod 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",
    "solid_oidc_supported": "https://solidproject.org/TR/solid-oidc",
    "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 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%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:

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 redirect_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"
    }
}

Token Body:

{
    "htu": "https://secureauth.example/token",
    "htm": "POST",
    "jti": "4ba3e9ef-e98d-4644-9878-7160fa7d3eb8",
    "iat": 1603306128
}

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

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"
}

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
}

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"
}

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
}

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"
}

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",
    }
}

Token Body:

{
    "htu": "https://bob.otherpod.example/private/photo_album.ttl",
    "htm": "GET",
    "jti": "fb1264dd-fff1-4509-a7b1-0fe58d08d3e1",
    "iat": 1603389616
}

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"
}

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.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Conformant Algorithms

Requirements phrased in the imperative as part of algorithms (such as "strip any leading space characters" or "return false and abort these steps") are to be interpreted with the meaning of the key word ("must", "should", "may", etc) used in introducing the algorithm.

Conformance requirements phrased as algorithms or specific steps can be implemented in any manner, so long as the end result is equivalent. In particular, the algorithms defined in this specification are intended to be easy to understand and are not intended to be performant. Implementers are encouraged to optimize.

References

Normative References

[OpenIDConnect]
N. Sakimura; et al. OpenID Connect Core 1.0. URL: https://openid.net/specs/openid-connect-core-1_0.html
[RDF11-CONCEPTS]
Richard Cyganiak; David Wood; Markus Lanthaler. RDF 1.1 Concepts and Abstract Syntax. 25 February 2014. REC. URL: https://www.w3.org/TR/rdf11-concepts/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[RFC6749]
D. Hardt, Ed.. The OAuth 2.0 Authorization Framework. October 2012. Proposed Standard. URL: https://datatracker.ietf.org/doc/html/rfc6749
[RFC7636]
N. Sakimura, Ed.; J. Bradley; N. Agarwal. Proof Key for Code Exchange by OAuth Public Clients. September 2015. Proposed Standard. URL: https://datatracker.ietf.org/doc/html/rfc7636
[Solid]
The Solid Ecosystem. URL: https://solid.github.io/specification/
[SolidOIDC]
A. Migus; R. White. Solid OIDC. URL: https://solid.github.io/authentication-panel/solid-oidc/
[WebID]
A. Sambra; H. Story; T. Berners-Lee. WebID 1.0. URL: https://dvcs.w3.org/hg/WebID/raw-file/tip/spec/identity-respec.html