Solid-OIDC Primer

Draft Community Group Report,

More details about this document
This version:
https://solid.github.io/solid-oidc/primer/
Latest published version:
https://solidproject.org/TR/oidc-primer
Created:
March 28, 2022
Modified:
22 June 2024
Feedback:
GitHub
Editors:
Jackson Morgan
Aaron Coburn
Matthieu Bosquet
elf Pavlik

Copyright © 2024 the Contributors to the Solid-OIDC Primer, published by the Solid Community Group under the W3C Community Contributor License Agreement (CLA). A human-readable summary is available.


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 report was published by the Solid Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

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 storage on a Solid compliant Resource Server as defined by 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]
code verifier

A cryptographically random string that is used to correlate the authorization request to the token request. [RFC7636]

code challenge

A challenge derived from the code verifier that is sent in the authorization request, to be verified against later. [RFC7636]

authorization code

The authorization code generated by the authorization server. [RFC6749]

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/client_id
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 verifier and code challenge

To follow the PKCE code flow we need to generate a code verifier and code challenge as instructed in the Proof Key for Code Exchange spec.

We start by generating a code verifier. This 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%client_id&
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/client_id, 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/client_id",
  "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 an authorization code

Generate a cryptographically random string that will be used as a authorization code (Let’s say ours is m-OrTPHdRsm8W_e9P0J2Bt). Store that string in a persistent 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/client_id",
    "code_challenge": "HSi9dwlvRpNHCDm-L8GOdM16qcb0tLHPZqQSvaWXTI0",
    "webid": "https://alice.coolpod.example/profile/card#me",
    "response_types": [ "code" ],
    "scope": [ "openid", "webid", "offline_access" ]
  }
}

11. Send authorization 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 authorization code that it will exchange for requested tokens.

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%2Fclient_id

Once this request is completed decentphotos can remove the code verifier from session storage.

15. Validate code verifier

The OP looks up the authorization 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 stored in the keystore. Proof Key for Code Exchange by OAuth Public Clients § section-4.6

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",
    "kid": "Xu68Q0ZfwDiRfWOb2UE8N77GoEQQ7q58_3gl1wsKENs"
}

Token Body:

{
    "sub": "https://alice.coolpod.example/profile/card#me",
    "aud": [ "solid", "https://decentphotos.example/client_id"],
    "azp": "https://decentphotos.example/client_id"
    "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 persistent 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 the Authorization Server, now the 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 ID 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 ID 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 id 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.

Details about the kid can be found in the JSON Web Signature (JWS) § section-4.1.4.

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, the client would need to upgrade RPT by pushing the 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.

Index

Terms defined by this specification

References

Normative References

[RFC7515]
M. Jones; J. Bradley; N. Sakimura. JSON Web Signature (JWS). May 2015. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc7515
[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

Informative 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. URL: https://w3c.github.io/rdf-concepts/spec/
[RFC6749]
D. Hardt, Ed.. The OAuth 2.0 Authorization Framework. October 2012. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc6749
[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://solid.github.io/solid-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/