We need to build a client that can work as a proxy on the server or inside a LauncherApp in the browser.
- JS Apps Launched by the LauncherApp will communicate with the wallet via
Windows.postMessage
to have headers signed, which the client can then use to make the request. - The Launcher App could
- sign the requests itself
- or make requests to a Personal Online Data Store (POD) proxy which could sign the requests before sending them on
The above shows that we will need authentication logic to function in the browser or on the server, so ideally we'd like to not duplicate the code.
The server can signal its support for any of these via a WWW-Authenticate header. Suppose we allow the following types of authentication.
- Basic: too simple not to implement;
- Digest: which should be the symmetric key authentication that a user uses for his own POD;
- HttpSig: the most efficient and secure authentication, best for connecting to other servers
- UMA: as described in Solid-OIDC is useful as a way of tying into widely deployed OAuth systems, but not efficient for highly decentralised apps.
- Credential based - find the spec
- Cookies: once a user is authenticated for a realm, using a cookie may be enough to continue the interaction. But cookie use in the browser by non-origin apps is limited and tricky.(see detailed course)
We will get going with 1 and 3, but keeping 2 and 4 in mind will be helpful to make sure the architecture is correct. In any case the system should be extensible so that others can contribute other auth schemes without problem.
When the client is accessing servers other than the user's POD, we extend the basic authentication diagram shown on the Mozilla Developer Network with an extra potential request to the Web Access Control (WAC) Document in 2, so that the client can find out which credential it could use, before authenticating:
Client WAC Resource
App Document Server
| | |
|-(1) request URL -------------------------------------------------->|
|<-----------(1a) 40x + WWW-Authenticate: HttpSig + (Link) to ACR --|
| | |
| | |
|-(2) GET WAC ------------------------------------->| |
|<-------------------(2a) 200 with rules -----------| |
| |
| (3) (select identity) |
| |
|-(4)- Add Authorization header or signature to new request -------->|
| |
| auth|
| verification|
| |
|<--------------------------------------------(4a) answer resource---|
We are only concerned about the client here, so we don't show what the server needs to do to verify the signed request in (4). Here are the main stages of the Client's behavior:
- The client makes a request which returns a 401 (1a) with a link the Web Access Control (WAC) document.
- it fetches the WAC doc to find out if any of the credentials it holds would be acceptable (eg. work or personal ID, age credential, ...). This may require fetching more than one (potentially cached) documents. Eg. it could require fetching links to groups referenced by the client, or even follow hierarchies downwards. Note that even when a client knows the password for the server it is connecting to, it may want to know the access control rule, as that could tell it to use another less powerful certificate.
- The client having selected the valid credentials and sorted them according to some preferences, the user can select one, or the wallet can follow some preferences expressed by that user previously.
- the client then adds the needed headers to the next request, making it an authenticated request by signing it in one way or another (eg. by adding a password, signature or token)
In http4s a Client is defined as a function from a Request[F]
to a Response[F]
, wrapped in a Resource type to take care of opening and closing of connections.
trait Client[F[_]]:
def run(req: Request[F]): Resource[F, Response[F]]
//...
This parallels the way Http4s defines server applications. As Ross Baker quipped in his introductory talk (video):
HTTP applications are just a Kleisli function from a streaming request to a polymorphic effect of a streaming response. So what's the problem?
This is just a little step beyond the definition
type HttpApp = Request[F] => F[Response[F]]
Where F
is the effect of the stream and the response,
usually IO
or Task
.
The difference between a server App and a client is that the client has to open a socket to the remote server (or find a cached connection) whereas for the server app, that has already been taken care of. Hence the client needs the Resource
type.
What we are constructing here, is what http4s calls a Client Middleware. That is a function that takes a Client and some extra data and returns a new Client.
A relevant example of a Middleware is the CookieJar which saves cookies to its DB and adds cookies to requests if available. Note that the rules for what cross origin JS can see of cookies is complex (see video. It cannot see Set-Cookie headers sent by the server for example. Q: could they still be useful for a server to reduce the need for authentication?)
The extra data we need is what we call a Wallet
trait Wallet[F[_]]:
def authn(failed: Response, lastReq: Request): F[Request]
The wallet can take a failed response (some 40x one) and the last Request - avoiding redirects - and by analysing the response for links to ACL headers, fetching those, find matching credentials in the Wallet, and after asking the user or his policies, authenticating the request by adding the needed headers to the request.
The Client Middleware thus takes a client - one that can correctly follow redirects perhaps - and transforms it into a new client that run
s a request, and if the returned Response
is 40x-ed can build an authenticated request using Wallet.authn
and then run that.
The AuthN Client MiddleWare will thus have a signature
def AuthNClient[F[_]](wallet: Wallet[F])(client: Client[F]): Client[F]
So now we can look at the components needed to build a Wallet. Ideally we would like this to be extensible so that if new authentication schemes come along it would be easy to fit them in.
Once the Wallet knows what Identity it should use, it can sign the request. All the Wallet needs to know when it signs a request is the following interface
trait AuthN:
def sign(originalReq: Request): Request
Different authentication methods will need different types of
data to construct their AuthN
function.
So for example Signing with a Password for a domain usually only requires the request, from which the Authority and protocol can be extracted, allowing one to return the request with the appropriate Authorize: Basic ...
header appended.
class Basic(id: UsernamePass) extends AuthN:
def sign(originalReq: Request): Request = ???
A function implementing HttpSig, will need to extract some data from the 40x Response object to check if it comes with an Accept-Signature header that would tell the client what headers it needs to sign. There may be other data needed too later such as a proof hint as to how that was determined.
class HttpSig(
keyId: KeyIdData,
acceptSig: Option[Header],
proof: Option[Proof]
):
def sign(originalReq: Request): Request = ???
We can think of similar functions for the other authentication protocols including Cookies.
Before getting to the point of signing a Request, we need to find out which credentials could be used. (We are using credentials here in a very general way, where a password or a public key counts as a credential)
We will need
- a function to fetch and interpret the WAC document into an RDF Named Graph, and the ability to follow links to other NGraphs.
- a function to search the for valid credentials. Starting from the WAC Named Graph, following links to other resources, and data in the Wallet DB, this should return an asynchronous Sequence of Credentials, with potentially proofs of how they fit the rules. Each credential may require searching the data differently.
def fetchWAC[F[_]](response: Response): F[Option[NGraph[Rdf]]]
This has to
- find a
Link: <doc.wac>, rel=acl
header in the Response - fetch the WAC document, in the cache or on the web - hence the need for the effect type
F
Once one has Web Access Control Document, one can find the rules that apply to the requested resource for the given mode.
def findValidRules(wac: NamedGraph, re: URL, mode: Mode): Seq[PNG] = ???
This will return a Seq of PointedNamedGraphs
which we will abbreviate with PNG
which is a triple of a NamedGraph and a pointer onto a node in the graph. The pointer should be the node of a wac:Authorization
where
@prefix wac: <http://www.w3.org/ns/auth/acl#>
From here on we just need ways to test if that node gives access to the user under specific types of descriptions.
Starting from a particular PNG on a wac:Authorization
we
return a collection of proofs.
trait IDVerifier:
type ID <: AgentID
def verify[F[_]](id: ID, rule: PNG): F[Seq[Proof]]
The result is wrapped in an effect Monad, as the verification will very often consist in Web or WebCache requests.
Different types of IDs will have different methods of providing
a proof, so we expect to have subclasses of IDVerifier
such as
class KeyIDVerifier extends IDVerifier:
type ID = KeyID
def verify[F[_]](id: KeyID, rule: PNG): F[Seq[Proof]] = ???
Other subclasses would be SolidOIDCVerifier
, or WebIDVerifier.
WebIDVerifier could be composed of others such as SolidOIDCVerifier
or KeyIDVerifier
or even verification via e-mail, since a WebID could have a description such as
<#i> a foaf:Person;
foaf:mbox <mailto:[email protected]>;
foaf:openid <https://openid.github.com/> ; # find correct url
cert:key </keys#k1> ;
solid:oidcIssuer <https://secureauth.example> .
Todo: define Proof. We may start with Proof of type 1
, so that we have either an empty sequence of a non-empty sequence, giving us the equivalent of a Boolean.
This may be done earlier too, filtering out early Ids that won't be
useful. A 401 response can come back with 0 or more WWW-Authenticate methods. Not all IDs compatible with the WAC file will be useable with the returned set of WWW-Authenticate
methods. Eg. At this point, a KeyId is not useable for Solid-OIDC and vice-versa.
Once the correct ID select, the appropriate AuthN
type can be used to sign the Request.