     [file version.texi not included]

This is the manual of webid-oidc (version VERSION, UPDATED), an implementation of the Solid authentication protocol for guile, client and server.

Copyright © 2020, 2021 Vivien Kraus

     Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled ``GNU Free Documentation License''

Webid-oidc
**********

Decentralized Authentication on the Web
***************************************

Authentication on the web is currently handled in the following way: anyone can install a server that will authenticate users on the web. The problem is interoperability. If a client (an application) wants to authenticate a user, it has to be approved by the authentication server. In other words, if USEFUL-PROGRAM wants to authenticate MEGACORP users, then USEFUL-PROGRAM has to register to MEGACORP first, and get approved. This goes against the principle of permission-less innovation, which is at the heart of the web.

In the decentralized authentication web, the best attempt so far is that of ActivityPub. All servers are interoperable with respect to authentication: if user A emits an activity, it is forwarded by A's server to its recipients, and A's server is responsible for A's identity.

The problem with that approach is that the data is tied to the application. It is not possible to use another application to process the data differently, or to use multiple data sources, in an interoperable way (without the ActivityPub server knowing). This means that on Activitypub, microblogging applications will not present different activities correctly. This also means that it is difficult to write a free replacement to a non-free application program, because it would need to manage the data.

In the Solid ecosystem, there is a clear distinction between servers and applications. An application is free to read data from all places at the same time, using a permission-less authentication system. Since the applications do not need to store data, the cost of having users is neglectible, so users do not need prior approval before using them (making captchas and the like a thing of the past). Servers do not have a say in which applications the user uses.

The authentication used is a slight modification of the well-established OpenID Connect. It is intended to work in a web browser, but this package demonstrates that it also works without a web browser.

The Json Web Token
******************

The Json Web Token, or JWT, is a terse representation of a pair of JSON objects: the header, and the payload. The JWT can be encoded as a Json Web Signature (JWS), in which case the header is encoded to base64 with the URL alphabet, and without padding characters, the payload is also encoded to base64, and the concatenation of the encoding of the header, a dot, and the encoding of the payload is signed with some cryptography algorithm. In the following, we will only be interested by public-key cryptography. The concatenation of header, dot, payload, dot and signature in base64 is the encoding of the JWT.

Decoded JWT are represented as a pair. The car of the pair is the header, and the cdr is the payload. Both the header and the payload use the JSON representation from srfi-180: objects are alists of symbols to values, arrays are vectors. It is unfortunate that guile-json has a slightly different representation, where alist keys are strings, but we hope that in the future SRFI-180 will be more closely respected.

The ID token
============

The ID token is a special JWT that the application keeps for itself. It is signed by the identity provider, and contains the following claims:

webid
     the URI of the user’s webid;

iss
     the URI of the identity provider (issuer);

sub
     the username (the webid-oidc issuer puts the webid again here, but it could be any string);

aud
     the ID of the client application that is intended to receive the ID token;

nonce
     some random data to change the signature;

exp
     an UTC time (in seconds) for when the token expires;

iat
     the time when it was issued.

There are functions to work with ID tokens in (webid-oidc oidc-id-token).

 -- function: id-token? OBJECT
     Check that OBJECT is a decoded ID token.

The following helper functions convert URIs to the URIs from (web uri) and times to (srfi srfi-19) dates.

 -- function: id-token-webid TOKEN
 -- function: id-token-iss TOKEN
 -- function: id-token-sub TOKEN
 -- function: id-token-aud TOKEN
 -- function: id-token-nonce TOKEN
 -- function: id-token-exp TOKEN
 -- function: id-token-iat TOKEN
     Get the suitable field from the payload of TOKEN.

ID tokens can be signed and encoded as a string, or decoded.

 -- function: id-token-decode TOKEN [#HTTP-GET]
     Decode TOKEN, as a string, into a decoded token. The signature verification will need to fetch the oidc configuration of the claimed issuer, and check the signature against the published keys. The http-get optional keyword argument can set a different implementation of http-get from (web client). Return #f if it failed, or the decoded token otherwise.

 -- function: id-token-encode TOKEN KEY
     Encode TOKEN and sign it with the issuer’s KEY.

 -- function: issue-id-token ISSUER-KEY #ALG #WEBID #ISS #SUB #AUD #EXP #IAT
     Create an ID token, and encode it with ISSUER-KEY.

The access token
================

The access token is obtained by the client through a token request, and is presented to the server on each authenticated request. It is signed by the identity provider, and it contains enough information so that the server knows who the user is and who the agent is, and most importantly the fingerprint of the key that the client should use in a DPoP proof.

The API is defined in (webid-oidc access-token).

 -- function: access-token? OBJECT
     Check that OBJECT is a decoded access token.

There are field getters for the access token:

 -- function: access-token-webid TOKEN
 -- function: access-token-iss TOKEN
 -- function: access-token-aud TOKEN
 -- function: access-token-exp TOKEN
 -- function: access-token-iat TOKEN
 -- function: access-token-cnf/jkt TOKEN
 -- function: access-token-client-id TOKEN
     Get the suitable field from the payload of TOKEN.

Access tokens can be signed and encoded as a string, or decoded.

 -- function: access-token-decode TOKEN [#HTTP-GET]
     Decode TOKEN, as a string, into a decoded token. As with the ID token, the signature verification will need to fetch the oidc configuration of the claimed issuer, and check the signature against the published keys. The http-get optional keyword argument can set a different implementation of http-get from (web client), for instance to re-use the what has been obtained by the ID token validation. Return #f if it failed, or the decoded token otherwise.

 -- function: access-token-encode TOKEN KEY
     Encode TOKEN and sign it with the issuer’s KEY.

 -- function: issue-access-token ISSUER-KEY #ALG #WEBID #ISS #EXP #IAT [#CLIENT-KEY | #CNF/JKT] #CLIENT-ID
     Create an access token, and encode it with ISSUER-KEY. You can either set the #:cnf/jkt keyword argument with the fingerprint of the client key, or set #:client-key directly, in which case the fingerprint will be computed for you.

The DPoP proof
==============

This is a special JWT, that is signed by a key controlled by the application. The access token certifies that the key used to sign the proof is approved by the identity provider.

 -- function: dpop-proof? PROOF
     Check that the PROOF is a decoded DPoP proof. The validity of the proof is not checked by this function.

 -- function: dpop-proof-alg PROOF
 -- function: dpop-proof-jwk PROOF
 -- function: dpop-proof-jti PROOF
 -- function: dpop-proof-htm PROOF
 -- function: dpop-proof-htu PROOF
 -- function: dpop-proof-iat PROOF
     Get the corresponding field of the proof.

 -- function: dpop-proof-decode CURRENT-TIME JTI-LIST METHOD URI STR CNF/CHECK
     Check and decode a DPoP proof encoded as STR.

     The CURRENT-TIME is passed as a date, time or number (of seconds).

     In order to prevent replay attacks, each proof has a unique random string that is remembered in JTI-LIST until its expiration date is reached. See the make-jti-list function.

     The proof is limited to the scope of one URI and one METHOD ('GET, 'POST and so on).

     Finally, the key that is used to sign the proof should be confirmed by the identity provider. To this end, the CNF/CHECK function is called with the fingerprint of the key. The function should check that the fingerprint is OK (return a boolean).

 -- function: make-jti-list
     This function in (webid-oidc jti-list) creates an in-memory, async-safe, thread-safe cache for the proof IDs.

 -- function: dpop-proof-encode PROOF KEY
     Encode the proof and sign it with KEY. To generate valid proofs, KEY should be the private key corresponding to the jwk field of the proof.

 -- function: issue-dpop-proof CLIENT-KEY #ALG #HTM #HTU #IAT
     Create a proof, sign it and encode it with CLIENT-KEY. CLIENT-KEY should contain both the private and public key, because the public part is written in the proof and the private part is used to sign it.

Generic JWTs
============

You can parse generic JWTs signed with JWS with the following functions from (webid-oidc jws).

 -- function: jws? JWT
     Check that JWT is a decoded JWT signed with JWS.

 -- function: jws-alg JWT
     Get the algorithm used to sign JWT.

 -- function: jws-decode STR LOOKUP-KEYS
     Check and decode a JWT signed with JWS and encoded as STR.

     Since the decoding and signature verification happen at the same time (for user friendliness), the LOOKUP-KEYS function is used. It is passed as arguments the decoded JWT (but the signature is not checked yet), and it should return a public key, a public key set or a list of public keys. If the key lookup failed, this function should raise an exception.

 -- function: jws-encode JWT KEY
     Encode the JWT and sign it with KEY.

Caching on server side
**********************

Both the identity provider and the resource server need to cache things. The identity provider will cache application webids, and the resource server will cache the identity provider keys, for instance.

The solution is to use a file-system cache. Every response (except those that have a cache-control policy of no-store) are stored to a sub-directory of XDG_CACHE_HOME. Each store has a 5% chance of triggering a cleanup of the cache. When a cleanup occurs, each cached response has a 5% chance of being dropped, including responses that are indicated as valid. This way, a malicious cache response that has a maliciously long validity will not stay too long in the cache. A log line will indicate which items are dropped.

The (webid-oidc cache) module exports two functions to deal with the cache.

 -- function: clean-cache [#PERCENTS] [#DIR]
     Drop PERCENTS% of the cache right now, in DIR (defaults to some place within XDG_CACHE_HOME).

 -- function: with-cache [#CURRENT-TIME] [#HTTP-GET] [#DIR]
     Return a function acting as http-get from (web client) (takes an URI as the first parameter, and an optional #:HEADERS set, and returns 2 values, the response and its body).

     The cache will be read and written in DIR (defaults to some place within XDG_CACHE_HOME), and the CURRENT-TIME number of seconds, SRFI-19 time or date, or time-returning thunk will be used to check for the validity of responses.

     The back-end function, HTTP-GET, defaults to that of (web client).

Exceptional conditions
**********************

The library will raise an exception whenever something fishy occurs. For instance, if a signature is invalid, or the expiration date has passed. All exception types are defined in (webid-oidc errors).

 -- function: error->str ERROR [#DEPTH]
     Return a string explaining the ERROR. You can limit the DEPTH of the explanation as an integer.

Invalid data format
===================

There are a few JSON objects with required fields. This exceptions usually occur as the cause of a higher-level exception.

 -- exception type: &not-base64 VALUE CAUSE
     This exception is raised when the base64 decoding function failed. VALUE is the incorrect input, and CAUSE is a low-level error.

 -- exception type: &not-json VALUE CAUSE
     Cannot decode VALUE to a JSON object.

 -- exception type: &not-turtle VALUE CAUSE
     Cannot decode VALUE to a RDF graph.

 -- exception type: &incorrect-webid-field VALUE
     The VALUE of the webid field in the JWT is missing (if #f), or not an acceptable value.

 -- exception type: &incorrect-iss-field VALUE
     The VALUE of the iss field is incorrect.

 -- exception type: &incorrect-aud-field VALUE
     The VALUE of the aud field is incorrect.

 -- exception type: &incorrect-iat-field VALUE
     The VALUE of the iat field is incorrect.

 -- exception type: &incorrect-exp-field VALUE
     The VALUE of the exp field is incorrect.

 -- exception type: &incorrect-cnf/jkt-field VALUE
     The VALUE of the cnf/jkt field is incorrect.

 -- exception type: &incorrect-client-id-field VALUE
     The VALUE of the client-id field is incorrect.

 -- exception type: &incorrect-typ-field VALUE
     The VALUE of the typ field in the DPoP proof header is incorrect.

 -- exception type: &incorrect-jwk-field VALUE CAUSE
     The VALUE of the jwk field in the DPoP proof header is incorrect.

 -- exception type: &incorrect-jti-field VALUE
     The VALUE of the jti field in the DPoP proof is incorrect.

 -- exception type: &incorrect-htm-field VALUE
     The VALUE of the htm field in the DPoP proof is incorrect.

 -- exception type: &incorrect-htu-field VALUE
     The VALUE of the htu field in the DPoP proof is incorrect.

 -- exception type: &incorrect-redirect-uris-field VALUE
     The VALUE of the redirect-uris field of a client manifest is incorrect.

 -- exception type: &incorrect-typ-field VALUE
     The VALUE of the typ field in the DPoP proof header is incorrect.

 -- exception type: &incorrect-sub-field VALUE
     The VALUE of the sub field is incorrect.

 -- exception type: &incorrect-iss-field VALUE
     The VALUE of the iss field is incorrect.

 -- exception type: &incorrect-nonce-field VALUE
     The VALUE of the nonce field in the DPoP proof is incorrect.

 -- exception type: &incorrect-htm-field VALUE
     The VALUE of the htm field in the DPoP proof is incorrect.

 -- exception type: &not-a-client-manifest VALUE CAUSE
     The CLIENT-MANIFEST is incorrect.

Invalid JWT
===========

Each JWT type – access token, DPoP proof, ID token, authorization code (this is internal to the identity provider) has different validation rules, and can fail in different ways.

 -- exception type: &unsupported-crv CRV
     The identifier CRV does not identify an elliptic curve.

 -- exception type: &not-a-jwk VALUE CAUSE
     VALUE does not identify a JWK.

 -- exception type: &not-a-public-jwk VALUE CAUSE
     VALUE does not identify a public JWK.

 -- exception type: &not-a-private-jwk VALUE CAUSE
     VALUE does not identify a private JWK.

 -- exception type: &not-a-jwks VALUE CAUSE
     VALUE does not identify a set of public keys.

 -- exception type: &unsupported-alg VALUE
     VALUE does not identify a valid hash algorithm.

 -- exception type: &invalid-signature KEY PAYLOAD SIGNATURE
     KEY has not signed PAYLOAD with SIGNATURE.

 -- exception type: &missing-alist-key VALUE KEY
     VALUE isn’t an alist, or is missing a value with KEY.

 -- exception type: &not-a-jws-header VALUE CAUSE
     VALUE does not identify a decoded JWS header.

 -- exception type: &not-a-jws-payload VALUE CAUSE
     VALUE does not identify a decoded JWS payload.

 -- exception type: &not-a-jws VALUE CAUSE
     VALUE does not identify a decoded JWS.

 -- exception type: &not-in-3-parts STRING SEPARATOR
     STRING cannot be split into 3 parts with SEPARATOR.

 -- exception type: &no-matching-key CANDIDATES ALG PAYLOAD SIGNATURE
     No key among CANDIDATES could verify SIGNATURE signed with ALG for PAYLOAD, because the signature mismatched for all keys.

 -- exception type: &cannot-decode-jws VALUE CAUSE
     The VALUE string is not an encoding of a valid JWS.

 -- exception type: &cannot-encode-jws JWS KEY CAUSE
     The JWS cannot be signed.

 -- exception type: &not-an-access-token VALUE CAUSE
     The VALUE is not an access token.

 -- exception type: &not-an-access-token-header VALUE CAUSE
     The VALUE is not an access token header.

 -- exception type: &not-an-access-token-payload VALUE CAUSE
     The VALUE is not an access token payload.

 -- exception type: &cannot-decode-access-token VALUE CAUSE
     The VALUE string is not an encoding of a valid access token.

 -- exception type: &cannot-encode-access-token ACCESS-TOKEN KEY CAUSE
     The ACCESS-TOKEN cannot be signed.

 -- exception type: &not-a-dpop-proof VALUE CAUSE
     The VALUE is not a DPoP proof.

 -- exception type: &not-a-dpop-proof-header VALUE CAUSE
     The VALUE is not a DPoP proof header.

 -- exception type: &not-a-dpop-proof-payload VALUE CAUSE
     The VALUE is not a DPoP proof payload.

 -- exception type: &cannot-decode-dpop-proof VALUE CAUSE
     The VALUE string is not an encoding of a valid DPoP proof.

 -- exception type: &cannot-encode-dpop-proof DPOP-PROOF KEY CAUSE
     The DPOP-PROOF cannot be signed.

 -- exception type: &not-an-authorization-code VALUE CAUSE
     The VALUE is not an authorization code.

 -- exception type: &not-an-authorization-code-header VALUE CAUSE
     The VALUE is not an authorization code header.

 -- exception type: &not-an-authorization-code-payload VALUE CAUSE
     The VALUE is not an authorization code payload.

 -- exception type: &cannot-decode-authorization-code VALUE CAUSE
     The VALUE string is not an encoding of a valid authorization code.

 -- exception type: &cannot-encode-authorization-code AUTHORIZATION-CODE KEY CAUSE
     The AUTHORIZATION-CODE cannot be signed.

 -- exception type: &not-an-id-token VALUE CAUSE
     The VALUE is not an ID token.

 -- exception type: &not-an-id-token-header VALUE CAUSE
     The VALUE is not an ID token header.

 -- exception type: &not-an-id-token-payload VALUE CAUSE
     The VALUE is not an ID token payload.

 -- exception type: &cannot-decode-id-token VALUE CAUSE
     The VALUE string is not an encoding of a valid ID token.

 -- exception type: &cannot-encode-id-token ID-TOKEN KEY CAUSE
     The ID-TOKEN cannot be signed.

Cannot fetch data on the web
============================

In the client (local and public parts), resource server and identity provider, the protocol requires to fetch data on the web.

 -- exception type: &request-failed-unexpectedly RESPONSE-CODE RESPONSE-REASON-PHRASE
     We expected the request to succeed, but the server sent a non-OK RESPONSE-CODE.

 -- exception type: &unexpected-header-value HEADER VALUE
     We did not expect the server to respond with HEADER set to VALUE.

 -- exception type: &unexpected-response RESPONSE CAUSE
     The RESPONSE (from (web response)) is not appropriate.

 -- exception type: &not-an-oidc-configuration VALUE CAUSE
     The VALUE is not appropriate an OIDC configuration.

 -- exception type: &cannot-fetch-issuer-configuration ISSUER CAUSE
     It is impossible to fetch the configuration of ISSUER.

 -- exception type: &cannot-fetch-jwks ISSUER URI CAUSE
     It is impossible to fetch the keys of ISSUER at URI.

 -- exception type: &cannot-fetch-linked-data URI CAUSE
     Could not fetch the graph referenced by URI.

 -- exception type: &cannot-fetch-client-manifest ID CAUSE
     Could not fetch a client manifest at ID.

Other errors in the protocol or from a reasonable implementation
================================================================

The protocol does not rely solely on JWT validation, so these errors may happen too.

 -- exception type: &dpop-method-mismatch SIGNED REQUESTED
     The method value SIGNED in the DPoP proof does not match the method that is REQUESTED on the server.

 -- exception type: &dpop-uri-mismatch SIGNED REQUESTED
     The URI value SIGNED in the DPoP proof does not match the URI that is REQUESTED on the server.

 -- exception type: &dpop-signed-in-future SIGNED CURRENT
     The proof is SIGNED for a date which is too much ahead of the CURRENT time.

 -- exception type: &dpop-too-old SIGNED CURRENT
     The proof was SIGNED at a past date of CURRENT.

 -- exception type: &dpop-unconfirmed-key KEY EXPECTED CAUSE
     The confirmation of KEY is not what is EXPECTED, or (if a function was passed as CNF/CHECK) the CAUSE exception occurred while confirming.

 -- exception type: &jti-found JTI CAUSE
     The JTI of the proof has already been issued in a recent past.

 -- exception type: &unauthorized-redirection-uri MANIFEST URI
     The authorization URI is not advertised in MANIFEST.

 -- exception type: &cannot-serve-public-manifest
     You cannot serve the public client manifest.

 -- exception type: &no-client-manifest-registration ID
     The ID client manifest does not have a registration triple in its document.

 -- exception type: &inconsistent-client-manifest-id ID ADVERTISED-ID
     The client MANIFEST is being fetched at ID, but it is valid for another client ADVERTISED-ID.

 -- exception type: &authorization-code-expired EXP CURRENT-TIME
     The authorization code has expired at EXP, it is now CURRENT-TIME.

 -- exception type: &invalid-refresh-token REFRESH-TOKEN
     The REFRESH-TOKEN is unknown to the identity provider.

 -- exception type: &invalid-key-for-refresh-token KEY JKT
     The refresh token was issued for JKT, but it is used with KEY.

 -- exception type: &unknown-client-locale WEB-LOCALE C-LOCALE
     The WEB-LOCALE of the client, translated to C as C-LOCALE, cannot be set. This exception is always continuable; if the handler returns, then the page will be served in the english locale.

GNU Free Documentation License
******************************

     [file fdl.texi not included]