Solid-OIDC Primer

Solid-OIDC Primer

Version 0.1.0, 2022-03-28

More details about this document
This version:
https://solidproject.org/TR/2022/oidc-primer-20220328
Latest published version:
https://solidproject.org/TR/oidc-primer
Editor's Draft:
https://solid.github.io/solid-oidc/primer/
Issue Tracking:
GitHub
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

This section describes the status of this document at the time of its publication.

This document was published by the Solid Community Group as Version 0.1.0. The sections that have been incorporated have been reviewed following the Solid process. However, the information in this document is still subject to change. You are invited to contribute any feedback, comments, or questions you might have.

Publication as Version 0.1.0 does not imply endorsement by the W3C Membership. This document and may be updated, replaced or obsoleted by other documents at any time. It is inappropriate to cite this document as other than work in progress.

This document was produced by a group operating under the W3C Community Contributor License Agreement (CLA). A human-readable summary is available.

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

OpenID Provider (OP)
An OAuth 2.0 authorization server implementing OpenID Connect as defined in the OpenID Connect Core 1.0 specification. [OIDC.Core]
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 Storage
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]
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 Storage, 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 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.
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 Client ID Document
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
Bob’s Storage
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:

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

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

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

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

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

Token Body:

{
    "htu": "https://auth.otherpod.example/token",
    "htm": "POST",
    "jti": "fb1264dd-fff1-4509-a7b1-0fe58d08d3e1",
    "iat": 1603389616
}

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.

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

[OIDC.Core]
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://datatracker.ietf.org/doc/html/rfc2119
[RFC6749]
D. Hardt, Ed.. The OAuth 2.0 Authorization Framework. October 2012. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc6749
[RFC7636]
N. Sakimura, Ed.; J. Bradley; N. Agarwal. Proof Key for Code Exchange by OAuth Public Clients. September 2015. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc7636
[RFC8414]
M. Jones; N. Sakimura; J. Bradley. OAuth 2.0 Authorization Server Metadata. June 2018. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8414
[Solid.OIDC]
Aaron Coburn; elf Pavlik ; Dmitri Zagidulin. Solid-OIDC. URL: https://solidproject.org/TR/oidc
[Solid.Protocol]
Sarven Capadisli; et al. Solid Protocol. URL: https://solidproject.org/TR/protocol
[WebID]
Andrei Sambra; Henry Story; Tim Berners-Lee. WebID 1.0. URL: https://www.w3.org/2005/Incubator/webid/spec/identity/