diff options
author | Vivien Kraus <vivien@planete-kraus.eu> | 2021-09-12 22:57:58 +0200 |
---|---|---|
committer | Vivien Kraus <vivien@planete-kraus.eu> | 2021-09-14 16:06:43 +0200 |
commit | 328b4957d05fc9b0f9ff87f2a4932ae0296ab069 (patch) | |
tree | 2d44b7896c91f9934b470fd6bb54141ddc4dc714 /doc | |
parent | 6a83b79c4de5986ad61a552c2612b7cce0105cda (diff) |
Restructure the client API
The client API had several problems:
- using records instead of GOOPS means that we aren’t flexible enough
to introduce accounts protected by a password, for a multi-user
application;
- saving the user database to disk means we can’t have a proper
immutable API;
- it was difficult to predict when the users database would change,
and inform the user interface about this change;
- it had two different ways to negociate an access token, one when we
had a refresh token and one when we did not;
- it was supposed to either use account objects or a subject / issuer
pair, now we only use account objects.
Diffstat (limited to 'doc')
-rw-r--r-- | doc/disfluid.texi | 248 |
1 files changed, 153 insertions, 95 deletions
diff --git a/doc/disfluid.texi b/doc/disfluid.texi index 04e69af..cf413af 100644 --- a/doc/disfluid.texi +++ b/doc/disfluid.texi @@ -864,81 +864,160 @@ authorization. Otherwise, raise an exception of type @node Running a client @chapter 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 list of accounts is stored on the file system. You can manipulate -the accounts with the @emph{(webid-oidc client accounts)} module. - -@deftp {Record type} <account> @var{subject} @var{issuer} @var{id-token} @var{access-token} @var{refresh-token} @var{keypair} -Store information about an account. @var{subject} is optional, -@var{issuer} is required, but they must both be URIs. In a typical -application, you would ask the user for per @var{issuer}, without -bothering perse with a webid (it can be long to type), and then start -making requests with this account. When you need an authorization -code, you will know the user’s webid. - -If the access token was not invalidated, then @var{id-token} contains -a (decrypted) identity token, and @var{access-token} an encrypted -access token. If you got a @var{refresh-token} for this account, it is -also stored, along with the @var{keypair} that is server-side bound to -it. - -The optional parameters are @code{#f} when we don’t have them. +The job of the client is to use accounts to fetch private resources on +the web. The @emph{(webid-oidc client)} defines the @code{<client>} +class. + +@deftp {Class} <client> @var{client-id} @var{key-pair} @var{redirect-uri} +In OIDC, a client is an application that does not hold the +resources. It may in fact be a network server available on the web, or +a program that you run on your machine. Being a network server or not +is irrelevant. + +The @code{<client>} class is designed with immutability in mind. You +can create a client with the @code{make} generic method, using these +keywords to initialize values: + +@table @code +@item #:client-id +to set the public client identifier (this endpoint +should be available on the world-wide web), as a string representing +an URI or an URI from @code{(web uri)}; +@item #:key-pair +to use a specific key pair. If not set, a new key pair will be +generated; +@item #:redirect-uri +to set the redirect URI that the application controls. It may just be +a page showing the authorization code, with instructions on how to +paste this code into the application. It should match one of the +authorized redirect URIs in the client identifier endpoint. + +If you want to set a state parameter for the redirection, you can do +it by setting the guile parameter @code{authorization-state}. +@end table @end deftp -@deffn function make-account @var{subject} @var{issuer} @var{id-token} @var{access-token} @var{refresh-token} @var{keypair} -Create an account. -@end deffn +@deftypefn {Generic method} uri client-id (@var{client} @code{<client>}) +@deftypefnx {Generic method} {key pair} client-key-pair (@var{client} @code{<client>}) +@deftypefnx {Generic method} uri client-redirect-uri (@var{client} @code{<client>}) +Slot accessors for @var{client}. +@end deftypefn -@deffn function account? @var{object} -Check whether @var{object} is an account. -@end deffn +@defvr {Parameter} client +Define this parameter to set the client to use to access private data. +@end defvr -@deffn function account-subject @var{account} -@deffnx function account-issuer @var{account} -@deffnx function account-id-token @var{account} -@deffnx function account-access-token @var{account} -@deffnx function account-refresh-token @var{account} -@deffnx function account-keypair @var{account} -Access the account. -@end deffn +To access private data, you must identify yourself. The +@emph{(webid-oidc client accounts)} module lets you define accounts. + +@deftp {Class} <account> @var{subject} @var{issuer} @var{id-token} @var{access-token} @var{refresh-token} @var{key-pair} +Encapsulate an account. @var{subject} is your webid, while +@var{issuer} is a host name. @var{id-token} is the @emph{decoded} OIDC ID token, i.e. a +pair of @code{(header . payload)}, because we don’t need to show it to +any other party, so its authenticity needs not be +demonstrated. However, @var{access-token} is an @emph{encoded} access +token (into a string), because we don’t need to worry about its +internals on client side. + +There are different ways to initialize an account. First, you can save +all parameters to some form of storage, and restore it by using the +associated keyword arguments at construction time: + +@table @code +@item #:subject +@item #:issuer +@item #:id-token +@item #:access-token +@item #:refresh-token +@item #:key-pair +@end table -You should always manage the accounts with the users database. +If you want to make a new account, you would ask the user for an +identity provider, and pass it with @code{#:issuer} as the only +initialized value. The constructor will log you in, using the +@code{authorization-process} and @code{anonymous-http-request} +function parameters. -@deffn function read-accounts -Read the list of all accounts. This function is safe to call at any -time during concurrent updates of the database. If the update was -finished, the new list is returned, otherwise the old list is returned -without blocking. -@end deffn +If you want to refresh an access token, you would also set +@code{#:refresh-token}. -@deffn function save-account @var{account} -Find an account in the database with the same subject and issuer, and -replace its contents with @var{account}. Return @var{account}. -@end deffn +In any case, when you don’t specify a value, it’s as if you passed +@code{#f}. +@end deftp -@deffn function delete-account @var{account} -Remove all accounts from the database that have the same subject and -issuer as @var{account}. -@end deffn +@defvr {Parameter} authorization-process +This function is called when an explicit user authorization is +required, for instance because there is no refresh token and the +access token expired. The function takes an URI as argument, with an +additional @code{#:issuer} keyword argument containing the issuer. In +this function, you should ask the user to browse this URI so that your +application gets the authorization code. +@end defvr -To log a user in, you must know at least per issuer. More precisely, -if the user is already known (because, for instance, the user presents -a cookie for your web application), then you should know the user’s -webid and the webid issuer. If you don’t know the user, and the user -is eligible to your service, then you will only know the identity -provider (issuer), because that’s what the user typed in. +@defvr {Parameter} anonymous-http-request +This function is used as a back-end for private resource access, and +to query the server configuration. It defaults to @code{http-request} +from @emph{(web client)}. +@end defvr -@deffn function login @var{subject} @var{issuer} [#:@var{http-request}=@code{http-request}] [#:@var{state}=@code{#f}] #:@var{client-id} #:@var{client-key} #:@var{redirect-uri} -Return a new account with an ID token and an access -token. @var{subject} is optional. +@deftypefn {Generic method} uri subject (@var{account} @code{<account>}) +@deftypefnx {Generic method} <account> set-subject (@var{account} @code{<account>}) (@var{uri} {string or URI}) +@deftypefnx {Generic method} uri issuer (@var{account} @code{<account>}) +@deftypefnx {Generic method} <account> set-issuer (@var{account} @code{<account>}) (@var{uri} {string or URI}) +@deftypefnx {Generic method} {optional decoded ID token} id-token (@var{account} @code{<account>}) +@deftypefnx {Generic method} <account> set-id-token (@var{account} @code{<account>}) (@var{id-token} {optional ID token}) +@deftypefnx {Generic method} {optional encoded access token} access-token (@var{account} @code{<account>}) +@deftypefnx {Generic method} <account> set-access-token (@var{account} @code{<account>}) (@var{access-token} {optional access token}) +@deftypefnx {Generic method} {optional <string>} refresh-token (@var{account} @code{<account>}) +@deftypefnx {Generic method} <account> set-refresh-token (@var{account} @code{<account>}) (@var{refresh-token} {optional <string>}) +@deftypefnx {Generic method} {key pair} key-pair (@var{account} @code{<account>}) +@deftypefnx {Generic method} <account> set-key-pair (@var{account} @code{<account>}) (@var{key-pair} {optional key pair}) +Slot accessors and functional setters for @var{account}. +@end deftypefn + +If you intend to run a public network server as a client application, +you may have multiple different users, but you should not let any user +use any account. If this is the case, you can either store the +accounts on the user agent storage (for instance, as a cookie), or +store all of them on the server. If you choose to store the accounts +on the user agent, at least use a new key pair for each of them. If +you want to store the user database on the server side, be aware that +no entity other than yourself will check that your user abides by any +term of service, so it is possible that a single user makes a lot of +accounts to annoy you and fill your hard drive with key pairs. If your +application does not let random people to use it, you might want to +use @emph{protected accounts}, to help you check that the users cannot +impersonate each other. + +@deftp {Class} <protected-account> (@code{<account>}) @var{username} @var{encrypted-password} +This superclass of @code{<account>} is protected by a username and +password. It is constructed with the initializer keywords +@code{#:username} and @code{#:encrypted-password}. +@end deftp -When you receive an account record from this function, make sure to -save it to the accounts database with @code{save-account}. -@end deffn +@deftypefn {Generic method} <string> username (@var{protected-account} @code{<protected-account>}) +@deftypefnx {Generic method} <protected-account> set-username (@var{protected-account} @code{<protected-account>}) (@var{username} <string>) +@deftypefnx {Generic method} <string> encrypted-password (@var{protected-account} @code{<protected-account>}) +@deftypefnx {Generic method} <protected-account> set-encrypted-password (@var{protected-account} @code{<protected-account>}) (@var{encrypted-password} <string>) +Slot accessors and functional setters for @var{protected-account}. +@end deftypefn + +@deftypefn {Generic method} <account> invalidate-access-token (@var{account} @code{<account>}) +Indicate that the access token in @var{account} cannot be used. Before +using @var{account} again, you will need to refresh the access +token. This function does not mutate @var{account}. +@end deftypefn + +@deftypefn {Generic method} <account> invalidate-refresh-token (@var{account} @code{<account>}) +Indicate that the refresh token has been revoked for +@var{account}. This is usually an indication that the user don’t want +your application to access her private data. This function does not +mutate @var{account}. +@end deftypefn + +@deftypefn {Generic method} <account> refresh (@var{account} @code{<account>}) +Refresh the access token. +@end deftypefn @deftp {Exception type} &authorization-code-required @var{uri} If the login process requires the user to send an authorization code, @@ -1007,40 +1086,19 @@ Constructor, predicate, and accessors for the @code{&token-request-failed} exception type. @end deffn -The @emph{(webid-oidc client)} module provides support for complete -clients. - -@deftp {Record type} <client> @var{id} @var{key} @var{redirect-uri} -The @var{id} of a client is an URI without fragment that can be -dereferenced in the world-wide web to metadata about the client. It -should allow @var{redirect-uri} to access the authorization code. +The @emph{(webid-oidc client)} module provides the most useful +function for a client. -It is useful if an application rotates its @var{key}. So, while a key -will still be used as long as the associated refresh token for a given -user is valid, you can equip new users with a new key pair. -@end deftp - -@deffn function make-client @var{id} @var{key} @var{redirect-uri} -@deffnx function client? @var{object} -@deffnx client-id @var{object} -@deffnx client-key @var{object} -@deffnx client-redirect-uri @var{object} -Constructor, predicate and accessors for the @code{<client>} record -type. +@deffn function request @var{account} @var{uri} . @var{args} +Perform a request on behalf of @var{account}, with the current value +of the @var{client} parameter as the client, using as a backend the +current value of @var{anonymous-http-request}. @end deffn -@deffn function initial-login @var{client} @var{issuer} [#:@var{http-request}] -Create an account by logging in with just the @var{issuer}, and save -the created account. The default @var{http-request} uses the cache for -GET requests. -@end deffn - -@deffn function request @var{client} @var{subject} @var{issuer} [#:@var{http-request}] -Log in with @var{subject} (optional, may be @code{#f}) and -@var{issuer}, and return a function that takes a request and request -body and transfers it, signed, to the @var{http-request} back-end. By -default, it uses the cache for GET requests. -@end deffn +Finally, to implement your application, there needs to be a public +endpoint for the resource server to check that you are not +impersonating another application. This endpoint can be served by any +web server, but a convenience procedure is made available here: @deffn function serve-application @var{id} @var{redirect-uri} @var{[#client-name]} @var{[#client-uri]} Return a handler for web requests to serve the application manifest |