for version ,

This is the manual of webid-oidc (version , ), 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''

Vivien Kraus Software libraries Decentralized Authentication on the Web.

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:

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

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.

Get the suitable field from the payload of token.

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

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.

Encode token and sign it with the issuer’s key.

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).

Check that object is a decoded access token.

There are field getters for the access token:

Get the suitable field from the payload of token.

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

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.

Encode token and sign it with the issuer’s key.

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.

Check that the proof is a decoded DPoP proof. The validity of the proof is not checked by this function.

Get the corresponding field of the proof.

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).

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

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.

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).

Check that jwt is a decoded JWT signed with JWS.

Get the algorithm used to sign jwt.

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.

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.

Drop percents% of the cache right now, in dir (defaults to some place within XDG_CACHE_HOME).

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).

What if something goes wrong?

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).

Return a string explaining the error. You can limit the depth of the explanation as an integer.

This exception is raised when the base64 decoding function failed. value is the incorrect input, and cause is a low-level error.

Cannot decode value to a JSON object.

Cannot decode value to a RDF graph.

The identifier crv does not identify an elliptic curve.

value does not identify a JWK.

value does not identify a public JWK.

value does not identify a private JWK.

value does not identify a set of public keys.

value does not identify a valid hash algorithm.

key has not signed payload with signature.

value isn’t an alist, or is missing a value with key.

value does not identify a decoded JWS header.

value does not identify a decoded JWS payload.

value does not identify a decoded JWS.

string cannot be split into 3 parts with separator.

No key among candidates could verify signature signed with alg for payload, because the signature mismatched for all keys.

The value string is not an encoding of a valid JWS.

The jws cannot be signed.

We expected the request to succeed, but the server sent a non-OK response-code.

We did not expect the server to respond with header set to value.

The response (from (web response)) is not appropriate.

The value is not an OIDC configuration.

The value of the webid field in the JWT is missing (if

#f
), or not an acceptable value.

The value of the sub field is incorrect.

The value of the iss field is incorrect.

The value of the aud field is incorrect.

The value of the iat field is incorrect.

The value of the exp field is incorrect.

The value of the cnf/jkt field is incorrect.

The value of the client-id field is incorrect.

The value of the redirect-uris field of a client manifest is incorrect.

The value of the typ field in the DPoP proof header is incorrect.

The value of the jwk field in the DPoP proof header is incorrect.

The value of the jti field in the DPoP proof is incorrect.

The value of the nonce field in the DPoP proof is incorrect.

The value of the htm field in the DPoP proof is incorrect.

The value of the htu field in the DPoP proof is incorrect.

The value is not an access token.

The value is not an access token header.

The value is not an access token payload.

It is impossible to fetch the configuration of issuer.

It is impossible to fetch the keys of issuer at uri.

The value string is not an encoding of a valid access token.

The access-token cannot be signed.

The value is not a DPoP proof.

The value is not a DPoP proof header.

The value is not a DPoP proof payload.

The method value signed in the DPoP proof does not match the method that is requested on the server.

The URI value signed in the DPoP proof does not match the URI that is requested on the server.

The proof is signed for a date which is too much ahead of the current time.

The proof was signed at a past date of current.

The confirmation of key is not what is expected, or (if a function was passed as cnf/check) the cause exception occurred while confirming.

The jti of the proof has already been issued in a recent past.

The value string is not an encoding of a valid DPoP proof.

The dpop-proof cannot be signed.

Could not fetch the graph referenced by uri.

The client-manifest is incorrect.

The authorization uri is not advertised in manifest.

You cannot serve the public client manifest.

The id client manifest does not have a registration triple in its document.

The client manifest is being fetched at id, but it is valid for another client advertised-id.

Could not fetch a client manifest at id.

The value is not an authorization code.

The value is not an authorization code header.

The value is not an authorization code payload.

The authorization code has expired at exp, it is now current-time.

The value string is not an encoding of a valid authorization code.

The authorization-code cannot be signed.

The refresh-token is unknown to the identity provider.

The refresh token was issued for jkt, but it is used with key.

The value is not an ID token.

The value is not an ID token header.

The value is not an ID token payload.

The value string is not an encoding of a valid ID token.

The id-token cannot be signed.

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.

The token request failed to indicate a value for the grant type, or indicated an unsupported grant type.

The token request forgot to put an authorization code.

The token request forgot to put a refresh token with the request.

provider is not confirmed by subject as an identity provider.

The webid cannot be certified by any identity providers. The causes alist indicates an error for each candidates.

The uri you passed to get an authorization code is neither an identity provider (because why-not-identity-provider) nor a webid (because why-not-webid).

The token request failed on the server.

The webid, as certified by iss, cannot be refreshed because we don’t have a refresh token stored in dir.

Running an Identity Provider

This project is packaged with a barebones identity provider. It has an authorization endpoint and a token endpoint (and it serves its public keys), but it is only intended for one specific person.

You can start it by invoking the

webid-oidc-issuer
program, with the following options:

The program is sensitive to the environment variables. The most important one is LANG, which influences how the program is internationalized to the server administrator (the pages served to the user use the user agent’s locale). This changes the long form of the options, and the language in the log files.

The XDG_DATA_HOME should point to some place where the program will store refresh tokens, under the

webid-oidc
directory. For a system service, you might want to define that environment to
/var/lib
, for instance.

The XDG_CACHE_HOME should point to a directory where to store the seed of the random number generator (under a

webid-oidc
directory, again). Changing the seed only happens when a program starts to require the random number generator. You can safely delete this directory, but you need to restart the program to actually change the seed.

Running a Resource Server

A Solid server is the server that manages your data. It needs to check that the proofs of possession are correct, and the possessed key is signed by the identity provider.

Running webid-oidc-reverse-proxy

The distribution comes with a reverse proxy, aptly named

webid-oidc-reverse-proxy
, to listen to an interface, take requests, authenticate them, and pass them to a backend with an additional header containing the webid of the agent, if authenticated.

The reverse proxy is invoked with the following arguments:

You can localize the interface by setting the LANG environment variable.

The authenticator

In (webid-oidc jws), the following function gives a simple API for a web server:

Create an authenticator, i.e. a function that takes a request and request body and returns the webid of the authenticated user, or

#f
if it is not authenticated.

To prevent replay attacks, each request is signed by the client with a different unique padding value. If such a value has already been seen, then the request must fail.

The authenticator expects the client to demonstrate the possession of a key that the identity provider knows. So the client creates a DPoP proof, targetted to a specific URI. In order to check that the URI is correct, the authenticator needs the public URI of the service.

The JTIs are checked within a small time frame. By default, the system time will be used. Otherwise, you can customize the

current-time
optional keyword argument, to pass a thunk returning a time from (srfi srfi-19).

You may want to customize the http-get optional keyword argument to pass a function to replace

http-get
from (http client). This function takes an URI and optional
#:headers
arguments, makes the request, and return two values: the response, and the response body.

This function, in (webid-oidc resource-server), returns a web request handler, taking the request and request body, and returning the subject of the access token. If an error happens, it is thrown; the function always returns a valid URI.

Running a client

To run a client, you need to proceed in two steps. First, acquire an OIDC ID token and an access token from the identity provider, and then present the access token and a proof of possession of the linked key in each request, in a DPoP HTTP header.

The first operation is performed by the (webid-oidc client) module.

The user enters a valid webid or a host name, and then this function will query it (with the http-get parameter, by default the web client from (web client)) to determine the authorization endpoint. The function will return an alist of authorization URIs, indexed by approved identity provider URIs, that the user should browse with a traditional web browser.

Each application should have its own webid, or in that case client-id, that can be dereferenced by the identity provider.

Once the user has given authorization, the user’s agent will be redirected to redirect-uri, with the authorization code as a GET parameter. It is possible to pass a state, but this is optional.

Once the client gets the authorization code, it is necessary to create an access token and ID token.

Trade an authorization-code, or a refresh-token, for an ID token and an access token bound to the client-key issued by host, the identity provider.

You can override the HTTP client used (http-get and http-post), and how to compute the time (current-time).

In an application, you would have a list of profiles in XDG_DATA_HOME, consisting of triples (webid, issuer, refresh token).

Read the list of available profiles. Returns a list of triples, webid, issuer, reresh token.

By default, this function will look for the profiles file in XDG_DATA_HOME. You can bypass it by providing the #dir optional keyword argument.

Negociate a refresh token, and save it. The function returns 3 values: the decoded ID token pyload, the encoded access token and the key pair.

The get-host/webid thunk should ask the user’s webid or identity provider, and return it. choose-provider is called with a list of possible identity providers as host names (strings), and the user should choose one. The chosen one is returned. Finally, browse-authorization-uri should ask or let the user browse an URI as its argument, and return the authorization code taken from the redirect URI.

The refresh token is saved to disk, as a profile, in XDG_DATA_HOME. Pass the optional #dir keyword argument to override the location.

You need to set client-id to the public webid of the app, and redirect-uri to one of the approved redirection URIs for the application ID.

If you have already a known profile, you can use it to automatically log in. This function might update the refresh token if it changed, so you can again set #dir. Please note that the refresh-token is bound to the client key on server side, so you must always use the same key.

If you have an ID token bound to a known profile, this helper function will look up the associated refresh token and log in.

Return a replacement of

http-request
from (web client), that automatically signs requests and refresh the tokens when needed.

#http-get and #http-post are only used to refresh the tokens, while #http-request is used as a back-end for the requests.

#current-time is set to a thunk that returns the time. It is used to issue DPoP proofs.

The identity provider needs to call the application on the web. So, your client should have a public endpoint on the web.

Return a handler for web requests to serve the application manifest and the redirection to transmit the authorization code. You should set the client-name to your application name and client-uri to point to where to a presentation of your application.

The

webid-oidc-client-service
program can run a server to serve these resources. It is invoked with the following options:

The program is sensitive to the environment variable LANG, which influences how the program is internationalized to the server administrator. This changes the long form of the options, and the language in the log files.

GNU Free Documentation License

Index