From 5c534490e022263848da5805a9f9cd8d657085aa Mon Sep 17 00:00:00 2001 From: Vivien Kraus Date: Sun, 18 Apr 2021 19:27:50 +0200 Subject: Negociate a token (client) --- doc/webid-oidc.texi | 119 ++++++++++ po/fr.po | 297 +++++++++++++------------ po/webid-oidc.pot | 264 ++++++++++++---------- src/scm/webid-oidc/Makefile.am | 6 +- src/scm/webid-oidc/cache.scm | 2 +- src/scm/webid-oidc/client.scm | 488 +++++++++++++++++++++++++++++++++++++++++ src/scm/webid-oidc/errors.scm | 68 +++++- tests/Makefile.am | 4 +- tests/client-authorization.scm | 118 ++++++++++ tests/client-token.scm | 121 ++++++++++ 10 files changed, 1218 insertions(+), 269 deletions(-) create mode 100644 src/scm/webid-oidc/client.scm create mode 100644 tests/client-authorization.scm create mode 100644 tests/client-token.scm diff --git a/doc/webid-oidc.texi b/doc/webid-oidc.texi index dda97bd..55a92d9 100644 --- a/doc/webid-oidc.texi +++ b/doc/webid-oidc.texi @@ -51,6 +51,7 @@ Free Documentation License'' * Caching on server side:: * Running an Identity Provider:: * Running a Resource Server:: +* Running a client:: * Exceptional conditions:: * GNU Free Documentation License:: * Index:: @@ -515,6 +516,104 @@ the subject of the access token. If an error happens, it is thrown; the function always returns a valid URI. @end deffn +@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 first operation is performed by the @emph{(webid-oidc client)} +module. + +@deffn function authorize @var{host/webid} @var{#client-id} @var{#redirect-uri} @var{[#state]} @var{[#http-get]} +The user enters a valid webid or a host name, and then this function +will query it (with the @var{http-get} parameter, by default the web +client from @emph{(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 +@var{client-id}, that can be dereferenced by the identity provider. + +Once the user has given authorization, the user’s agent will be +redirected to @var{redirect-uri}, with the authorization code as a GET +parameter. It is possible to pass a @var{state}, but this is optional. +@end deffn + +Once the client gets the authorization code, it is necessary to create +an access token and ID token. + +@deffn function token @var{host} @var{client-key} @var{[#authorization-code]} @var{[#refresh-token]} @var{[#http-get]} @var{[#http-post]} @var{[#current-time]} +Trade an @var{authorization-code}, or a @var{refresh-token}, for an ID +token and an access token bound to the @var{client-key} issued by +@var{host}, the identity provider. + +You can override the HTTP client used (@var{http-get} and +@var{http-post}), and how to compute the time (@var{current-time}). +@end deffn + +In an application, you would have a list of profiles in XDG_DATA_HOME, +consisting of triples (webid, issuer, refresh token). + +@deffn function list-profiles @var{[#dir]} +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 +@var{XDG_DATA_HOME}. You can bypass it by providing the @var{#dir} +optional keyword argument. +@end deffn + +@deffn function setup @var{get-host/webid} @var{choose-provider} @var{browse-authorization-uri} @var{#client-id} @var{#redirect-uri} @var{[#dir]} @var{[#http-get]} @var{[#http-post]} @var{[#current-time]} +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 @var{get-host/webid} thunk should ask the user’s webid or identity +provider, and return it. @var{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, +@var{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 @var{#dir} keyword argument to +override the location. + +You need to set @var{client-id} to the public webid of the app, and +@var{redirect-uri} to one of the approved redirection URIs for the +application ID. +@end deffn + +@deffn function login @var{webid} @var{issuer} @var{refresh-token} @var{key} @var{[#dir]} @var{[#http-get]} @var{[#http-post]} @var{[#current-time]} +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 @var{#dir}. Please note that the @var{refresh-token} +is bound to the client @var{key} on server side, so you must always +use the same @var{key}. +@end deffn + +@deffn function refresh @var{id-token} @var{key} @var{[#dir]} @var{[#http-get]} @var{[#http-post]} @var{[#current-time]} +If you have an ID token bound to a known profile, this helper function +will look up the associated refresh token and log in. +@end deffn + +@deffn function make-client @var{id-token} @var{access-token} @var{key} @var{[#dir]} @var{[#http-get]} @var{[#http-post]} @var{[#http-request]} @var{[#current-time]} +Return a replacement of @code{http-request} from @emph{(web client)}, +that automatically signs requests and refresh the tokens when needed. + +@var{#http-get} and @var{#http-post} are only used to refresh the +tokens, while @var{#http-request} is used as a back-end for the +requests. + +@var{#current-time} is set to a thunk that returns the time. It is +used to issue DPoP proofs. +@end deffn + @node Exceptional conditions @chapter Exceptional conditions @@ -913,6 +1012,26 @@ The token request forgot to put a refresh token with the request. provider. @end deftp +@deftp {exception type} &no-provider-candidates @var{webid} @var{causes} +The @var{webid} cannot be certified by any identity providers. The +@var{causes} alist indicates an error for each candidates. +@end deftp + +@deftp {exception type} &neither-identity-provider-nor-webid @var{uri} @var{why-not-identity-provider} @var{why-not-webid} +The @var{uri} you passed to get an authorization code is neither an +identity provider (because @var{why-not-identity-provider}) nor a +webid (because @var{why-not-webid}). +@end deftp + +@deftp {exception type} &token-request-failed @var{cause} +The token request failed on the server. +@end deftp + +@deftp {exception type} &profile-not-found @var{webid} @var{iss} @var{dir} +The @var{webid}, as certified by @var{iss}, cannot be refreshed +because we don’t have a refresh token stored in @var{dir}. +@end deftp + @node GNU Free Documentation License @appendix GNU Free Documentation License diff --git a/po/fr.po b/po/fr.po index 222f4cf..d2ee64b 100644 --- a/po/fr.po +++ b/po/fr.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: webid-oidc 0.0.0\n" "Report-Msgid-Bugs-To: vivien@planete-kraus.eu\n" -"POT-Creation-Date: 2021-06-05 16:20+0200\n" +"POT-Creation-Date: 2021-06-05 16:21+0200\n" "PO-Revision-Date: 2021-06-05 11:07+0200\n" "Last-Translator: Vivien Kraus \n" "Language-Team: French \n" @@ -126,101 +126,101 @@ msgstr "Utilisation : generate-random [NOMBRE D'OCTETS]\n" msgid "Usage: generate-key [NUMBER OF BITS | CURVE]\n" msgstr "Utilisation : generate-key [NOMBRE DE BITS | COURBE]\n" -#: src/scm/webid-oidc/errors.scm:839 +#: src/scm/webid-oidc/errors.scm:881 msgid "that’s how it is" msgstr "c’est comme ça" -#: src/scm/webid-oidc/errors.scm:844 +#: src/scm/webid-oidc/errors.scm:886 #, scheme-format msgid "the value ~s is not a base64 string (because ~a)" msgstr "la valeur ~s n’est pas une chaîne base64 (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:847 +#: src/scm/webid-oidc/errors.scm:889 #, scheme-format msgid "the value ~s is not JSON (because ~a)" msgstr "la valeur ~s n’est pas du JSON (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:850 +#: src/scm/webid-oidc/errors.scm:892 #, scheme-format msgid "the value ~s is not Turtle (because ~a)" msgstr "la valeur ~s n’est pas du Turtle (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:853 +#: src/scm/webid-oidc/errors.scm:895 #, scheme-format msgid "the value ~s does not identify an elleptic curve" msgstr "la valeur ~s n’identifie pas une courbe elliptique" -#: src/scm/webid-oidc/errors.scm:858 +#: src/scm/webid-oidc/errors.scm:900 #, scheme-format msgid "the value ~s does not identify a JWK (because ~a)" msgstr "la valeur ~s n’identifie pas une JWK (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:860 +#: src/scm/webid-oidc/errors.scm:902 #, scheme-format msgid "the value ~s does not identify a JWK" msgstr "la valeur ~s n’identifie pas une JWK" -#: src/scm/webid-oidc/errors.scm:865 +#: src/scm/webid-oidc/errors.scm:907 #, scheme-format msgid "the value ~s does not identify a public JWK (because ~a)" msgstr "la valeur ~s n’identifie pas une JWK publique (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:867 +#: src/scm/webid-oidc/errors.scm:909 #, scheme-format msgid "the value ~s does not identify a public JWK" msgstr "la valeur ~s n’identifie pas une JWK publique" -#: src/scm/webid-oidc/errors.scm:872 +#: src/scm/webid-oidc/errors.scm:914 #, scheme-format msgid "the value ~s does not identify a private JWK (because ~a)" msgstr "la valeur ~s n’identifie pas une JWK privée (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:874 +#: src/scm/webid-oidc/errors.scm:916 #, scheme-format msgid "the value ~s does not identify a private JWK" msgstr "la valeur ~s n’identifie pas une JWK privée" -#: src/scm/webid-oidc/errors.scm:879 +#: src/scm/webid-oidc/errors.scm:921 #, scheme-format msgid "the value ~s does not identify a JWKS (because ~a)" msgstr "la valeur ~s n’identifie pas un JWKS (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:881 +#: src/scm/webid-oidc/errors.scm:923 #, scheme-format msgid "the value ~s does not identify a JWKS" msgstr "la valeur ~s n’identifie pas un JWKS" -#: src/scm/webid-oidc/errors.scm:884 +#: src/scm/webid-oidc/errors.scm:926 #, scheme-format msgid "the value ~s does not identify a hash algorithm" msgstr "la valeur ~s n’identifie pas un algorithme de hachage" -#: src/scm/webid-oidc/errors.scm:887 +#: src/scm/webid-oidc/errors.scm:929 #, scheme-format msgid "the value ~s is not an alist or misses key ~s" msgstr "la valeur ~s n’est pas une alist ou il manque la clé ~s" -#: src/scm/webid-oidc/errors.scm:890 +#: src/scm/webid-oidc/errors.scm:932 #, scheme-format msgid "the value ~s is not a JWS header (because ~a)" msgstr "la valeur ~s n’est pas un header JWS (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:893 +#: src/scm/webid-oidc/errors.scm:935 #, scheme-format msgid "the value ~s is not a JWS payload (because ~a)" msgstr "la valeur ~s n’est pas un contenu JWS (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:896 +#: src/scm/webid-oidc/errors.scm:938 #, scheme-format msgid "the value ~s is not a JWS (because ~a)" msgstr "la valeur ~s n’est pas un JWS (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:899 +#: src/scm/webid-oidc/errors.scm:941 #, scheme-format msgid "the string ~s cannot be split in 3 parts with ~s" msgstr "la chaîne ~s ne peut pas être découpée en 3 parties avec ~s" -#: src/scm/webid-oidc/errors.scm:902 +#: src/scm/webid-oidc/errors.scm:944 #, scheme-format msgid "" "all key candidates failed to verify signature ~s with algorithm ~s and " @@ -229,17 +229,17 @@ msgstr "" "aucune clé candidate n’a pu vérifier la signature ~s avec l’algorithme ~s et " "le contenu ~a (il y en avait ~a : ~s)" -#: src/scm/webid-oidc/errors.scm:905 +#: src/scm/webid-oidc/errors.scm:947 #, scheme-format msgid "I cannot decode JWS ~a (because ~a)" msgstr "je n’ai pas pu décoder le JWS encodé par ~a (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:908 +#: src/scm/webid-oidc/errors.scm:950 #, scheme-format msgid "I cannot encode JWS ~a (because ~a)" msgstr "je n’ai pas pu encoder le JWS ~a (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:911 +#: src/scm/webid-oidc/errors.scm:953 #, scheme-format msgid "" "the server request unexpectedly failed with code ~a and reason phrase ~s" @@ -247,338 +247,338 @@ msgstr "" "la requête au serveur a échoué de façon inattendue avec un code ~a et une " "raison ~s" -#: src/scm/webid-oidc/errors.scm:916 +#: src/scm/webid-oidc/errors.scm:958 #, scheme-format msgid "the header ~a should not have the value ~s" msgstr "l’en-tête ~a ne devrait pas avoir la valeur ~s" -#: src/scm/webid-oidc/errors.scm:918 +#: src/scm/webid-oidc/errors.scm:960 #, scheme-format msgid "the header ~a should be present" msgstr "l’en-tête ~a devrait être présent" -#: src/scm/webid-oidc/errors.scm:921 +#: src/scm/webid-oidc/errors.scm:963 #, scheme-format msgid "the server response wasn't expected: ~s (because ~a)" msgstr "la réponse du serveur est inattendue : ~s (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:927 +#: src/scm/webid-oidc/errors.scm:969 #, scheme-format msgid "the value ~s is not an OIDC configuration (because ~a)" msgstr "la valeur ~s n’est pas une configuration OIDC (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:932 +#: src/scm/webid-oidc/errors.scm:974 #, scheme-format msgid "the webid field is incorrect: ~s" msgstr "le champ webid est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:933 +#: src/scm/webid-oidc/errors.scm:975 msgid "the webid field is missing" msgstr "le champ webid est manquant" -#: src/scm/webid-oidc/errors.scm:937 +#: src/scm/webid-oidc/errors.scm:979 #, scheme-format msgid "the sub field is incorrect: ~s" msgstr "le champ sub est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:938 +#: src/scm/webid-oidc/errors.scm:980 msgid "the sub field is missing" msgstr "le champ sub est manquant" -#: src/scm/webid-oidc/errors.scm:942 +#: src/scm/webid-oidc/errors.scm:984 #, scheme-format msgid "the iss field is incorrect: ~s" msgstr "le champ iss est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:943 +#: src/scm/webid-oidc/errors.scm:985 msgid "the iss field is missing" msgstr "le champ iss est manquant" -#: src/scm/webid-oidc/errors.scm:947 +#: src/scm/webid-oidc/errors.scm:989 #, scheme-format msgid "the aud field is incorrect: ~s" msgstr "le champ aud est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:948 +#: src/scm/webid-oidc/errors.scm:990 msgid "the aud field is missing" msgstr "le champ aud est manquant" -#: src/scm/webid-oidc/errors.scm:952 +#: src/scm/webid-oidc/errors.scm:994 #, scheme-format msgid "the iat field is incorrect: ~s" msgstr "le champ iat est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:953 +#: src/scm/webid-oidc/errors.scm:995 msgid "the iat field is missing" msgstr "le champ iat est manquant" -#: src/scm/webid-oidc/errors.scm:957 +#: src/scm/webid-oidc/errors.scm:999 #, scheme-format msgid "the exp field is incorrect: ~s" msgstr "le champ exp est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:958 +#: src/scm/webid-oidc/errors.scm:1000 msgid "the exp field is missing" msgstr "le champ exp est manquant" -#: src/scm/webid-oidc/errors.scm:962 +#: src/scm/webid-oidc/errors.scm:1004 #, scheme-format msgid "the cnf/jkt field is incorrect: ~s" msgstr "le champ cnf/jkt est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:963 +#: src/scm/webid-oidc/errors.scm:1005 msgid "the cnf/jkt field is missing" msgstr "le champ cnf/jkt est manquant" -#: src/scm/webid-oidc/errors.scm:967 +#: src/scm/webid-oidc/errors.scm:1009 #, scheme-format msgid "the client-id field is incorrect: ~s" msgstr "le champ client-id est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:968 +#: src/scm/webid-oidc/errors.scm:1010 msgid "the client-id field is missing" msgstr "le champ client-id est manquant" -#: src/scm/webid-oidc/errors.scm:972 +#: src/scm/webid-oidc/errors.scm:1014 #: src/scm/webid-oidc/authorization-page-unsafe.scm:133 #, scheme-format msgid "the redirect_uris field is incorrect: ~s" msgstr "le champ redirect_uris est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:973 +#: src/scm/webid-oidc/errors.scm:1015 #: src/scm/webid-oidc/authorization-page-unsafe.scm:134 msgid "the redirect_uris field is missing" msgstr "le champ redirect_uris est manquant" -#: src/scm/webid-oidc/errors.scm:977 +#: src/scm/webid-oidc/errors.scm:1019 #, scheme-format msgid "the typ field is incorrect: ~s" msgstr "le champ typ est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:978 +#: src/scm/webid-oidc/errors.scm:1020 msgid "the typ field is missing" msgstr "le champ typ est manquant" -#: src/scm/webid-oidc/errors.scm:982 +#: src/scm/webid-oidc/errors.scm:1024 #, scheme-format msgid "the jwk field is incorrect: ~s (because ~a)" msgstr "le champ jwk est incorrect : ~s (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:984 +#: src/scm/webid-oidc/errors.scm:1026 msgid "the jwk field is missing" msgstr "le champ jwk est manquant" -#: src/scm/webid-oidc/errors.scm:988 +#: src/scm/webid-oidc/errors.scm:1030 #, scheme-format msgid "the jti field is incorrect: ~s" msgstr "le champ jti est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:989 +#: src/scm/webid-oidc/errors.scm:1031 msgid "the jti field is missing" msgstr "le champ jti est manquant" -#: src/scm/webid-oidc/errors.scm:993 +#: src/scm/webid-oidc/errors.scm:1035 #, scheme-format msgid "the nonce field is incorrect: ~s" msgstr "le champ nonce est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:994 +#: src/scm/webid-oidc/errors.scm:1036 msgid "the nonce field is missing" msgstr "le champ nonce est manquant" -#: src/scm/webid-oidc/errors.scm:998 +#: src/scm/webid-oidc/errors.scm:1040 #, scheme-format msgid "the htm field is incorrect: ~s" msgstr "le champ htm est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:999 +#: src/scm/webid-oidc/errors.scm:1041 msgid "the htm field is missing" msgstr "le champ htm est manquant" -#: src/scm/webid-oidc/errors.scm:1003 +#: src/scm/webid-oidc/errors.scm:1045 #, scheme-format msgid "the htu field is incorrect: ~s" msgstr "le champ htu est incorrect : ~s" -#: src/scm/webid-oidc/errors.scm:1004 +#: src/scm/webid-oidc/errors.scm:1046 msgid "the htu field is missing" msgstr "le champ htu est manquant" -#: src/scm/webid-oidc/errors.scm:1006 +#: src/scm/webid-oidc/errors.scm:1048 #, scheme-format msgid "~s is not an access token (because ~a)" msgstr "~s n’est pas un jeton d’accès (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1009 +#: src/scm/webid-oidc/errors.scm:1051 #, scheme-format msgid "~s is not an access token header (because ~a)" msgstr "~s n’est pas un en-tête de jeton d’accès (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1012 +#: src/scm/webid-oidc/errors.scm:1054 #, scheme-format msgid "~s is not an access token payload (because ~a)" msgstr "~s n’est pas un contenu de jeton d’accès (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1015 +#: src/scm/webid-oidc/errors.scm:1057 #, scheme-format msgid "~s is not a DPoP proof (because ~a)" msgstr "~s n’est pas une preuve DPoP (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1018 +#: src/scm/webid-oidc/errors.scm:1060 #, scheme-format msgid "~s is not a DPoP proof header (because ~a)" msgstr "~s n’est pas un en-tête de preuve DPoP (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1021 +#: src/scm/webid-oidc/errors.scm:1063 #, scheme-format msgid "~s is not a DPoP proof payload (because ~a)" msgstr "~s n’est pas un contenu de preuve DPoP (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1024 +#: src/scm/webid-oidc/errors.scm:1066 #, scheme-format msgid "I cannot fetch the issuer configuration of ~a (because ~a)" msgstr "" "je n’ai pas pu récupérer la configuration de l’émetteur ~a (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1031 +#: src/scm/webid-oidc/errors.scm:1073 #, scheme-format msgid "I cannot fetch the JWKS of ~a at ~a (because ~a)" msgstr "je n’ai pas pu récupérer le JWKS de ~a à ~a (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1042 +#: src/scm/webid-oidc/errors.scm:1084 #, scheme-format msgid "the HTTP method is signed for ~s, but ~s was requested" msgstr "la méthode HTTP a été signée pour ~s, mais ~s a été demandé" -#: src/scm/webid-oidc/errors.scm:1045 +#: src/scm/webid-oidc/errors.scm:1087 #, scheme-format msgid "the HTTP uri is signed for ~a, but ~a was requested" msgstr "l’uri HTTP a été signé pour ~a, mais ~a a été demandé" -#: src/scm/webid-oidc/errors.scm:1048 +#: src/scm/webid-oidc/errors.scm:1090 #, scheme-format msgid "the date is ~a, but the DPoP proof is signed in the future at ~a" msgstr "la date est ~a, mais la preuve DPoP a été signée dans le futur à ~a" -#: src/scm/webid-oidc/errors.scm:1052 +#: src/scm/webid-oidc/errors.scm:1094 #, scheme-format msgid "the date is ~a, but the DPoP proof was signed too long ago at ~a" msgstr "" "la date est ~a, mais la preuve DPoP a été signée il y a trop longtemps à ~a" -#: src/scm/webid-oidc/errors.scm:1061 +#: src/scm/webid-oidc/errors.scm:1103 #, scheme-format msgid "the key ~s does not hash to ~a" msgstr "la clé ~s ne donne pas un hash de ~a" -#: src/scm/webid-oidc/errors.scm:1063 +#: src/scm/webid-oidc/errors.scm:1105 #, scheme-format msgid "the key confirmation of ~s failed (because ~a)" msgstr "la confirmation de clé de ~s a échoué (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1065 +#: src/scm/webid-oidc/errors.scm:1107 #, scheme-format msgid "the key confirmation of ~s failed" msgstr "la confirmation de la clé ~s a échoué" -#: src/scm/webid-oidc/errors.scm:1067 +#: src/scm/webid-oidc/errors.scm:1109 #, scheme-format msgid "the jti ~s has already been found (because ~a)" msgstr "le jti ~s a déjà été trouvé (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1070 +#: src/scm/webid-oidc/errors.scm:1112 #, scheme-format msgid "I cannot decode ~s as an access token (because ~a)" msgstr "je n’ai pas pu décoder ~s comme jeton d’accès (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1073 +#: src/scm/webid-oidc/errors.scm:1115 #, scheme-format msgid "I cannot encode ~s as an access token with key ~s (because ~a)" msgstr "" "je n’ai pas pu encoder ~s comme un jeton d’accès avec la clé ~s (parce que " "~a)" -#: src/scm/webid-oidc/errors.scm:1076 +#: src/scm/webid-oidc/errors.scm:1118 #, scheme-format msgid "I cannot decode ~s as a DPoP proof (because ~a)" msgstr "je n’ai pas pu décoder ~s comme preuve DPoP (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1079 +#: src/scm/webid-oidc/errors.scm:1121 #, scheme-format msgid "I cannot encode ~s as a DPoP proof (because ~a)" msgstr "je n’ai pas pu encoder ~s comme une preuve DPoP (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1082 +#: src/scm/webid-oidc/errors.scm:1124 #, scheme-format msgid "I could not fetch a RDF graph at ~a (because ~a)" msgstr "je n’ai pas pu récupérer de graphe RDF à ~a (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1085 +#: src/scm/webid-oidc/errors.scm:1127 #, scheme-format msgid "~s is not a client manifest (because ~a)" msgstr "~s n’est pas un manifeste client (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1088 +#: src/scm/webid-oidc/errors.scm:1130 #, scheme-format msgid "~s does not authorize redirection URI ~a" msgstr "~s n’autorise pas l’URI de redirection ~a" -#: src/scm/webid-oidc/errors.scm:1091 +#: src/scm/webid-oidc/errors.scm:1133 msgid "I cannot serve a public manifest" msgstr "je ne peux pas servir un manifeste public" -#: src/scm/webid-oidc/errors.scm:1093 +#: src/scm/webid-oidc/errors.scm:1135 #, scheme-format msgid "~a does not have a client manifest registration triple" msgstr "~a n’a pas de triplet d’enregistrement de manifeste client" -#: src/scm/webid-oidc/errors.scm:1096 +#: src/scm/webid-oidc/errors.scm:1138 #, scheme-format msgid "the client manifest at ~a is advertised for ~a" msgstr "le manifeste client ~a est publié pour ~a" -#: src/scm/webid-oidc/errors.scm:1099 +#: src/scm/webid-oidc/errors.scm:1141 #, scheme-format msgid "I could not fetch the client manifest of ~a (because ~a)" msgstr "je n’ai pas pu récupérer le manifeste client de ~a (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1102 +#: src/scm/webid-oidc/errors.scm:1144 #, scheme-format msgid "~s is not an authorization code (because ~a)" msgstr "~s n’est pas un code d’autorisation (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1105 +#: src/scm/webid-oidc/errors.scm:1147 #, scheme-format msgid "~s is not an authorization code header (because ~a)" msgstr "~s n’est pas un en-tête de code d’autorisation (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1108 +#: src/scm/webid-oidc/errors.scm:1150 #, scheme-format msgid "~s is not an authorization code payload (because ~a)" msgstr "~s n’est pas un contenu de code d’autorisation (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1111 +#: src/scm/webid-oidc/errors.scm:1153 #, scheme-format msgid "the current time is ~a, and the authorization code expired at ~a" msgstr "" "la date est actuellement ~a, et le code d’autorisation a expiré à la date ~a" -#: src/scm/webid-oidc/errors.scm:1115 +#: src/scm/webid-oidc/errors.scm:1157 #, scheme-format msgid "I cannot decode ~s as an authorization code (because ~a)" msgstr "je n’ai pas pu décoder ~s comme un code d’autorisation (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1118 +#: src/scm/webid-oidc/errors.scm:1160 #, scheme-format msgid "I cannot encode ~s as an authorization code (because ~a)" msgstr "je n’ai pas pu encoder ~s comme un code d’autorisation (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1121 +#: src/scm/webid-oidc/errors.scm:1163 #, scheme-format msgid "there is no such refresh token as ~s" msgstr "il n’y a pas de jeton de rafraîchissement ~s" -#: src/scm/webid-oidc/errors.scm:1124 +#: src/scm/webid-oidc/errors.scm:1166 #, scheme-format msgid "" "the refresh token is bound to a key confirmed as ~s, but it is used with key " @@ -587,45 +587,45 @@ msgstr "" "Le jeton de rafraîchissement est lié à une clé confirmée par ~s, mais il est " "utilisé avec la clé ~s" -#: src/scm/webid-oidc/errors.scm:1127 +#: src/scm/webid-oidc/errors.scm:1169 #, scheme-format msgid "I cannot decode ~s as an ID token (because ~a)" msgstr "je n’ai pas pu décoder ~s comme jeton d’identité (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1130 +#: src/scm/webid-oidc/errors.scm:1172 #, scheme-format msgid "I cannot encode ~s as an ID token (because ~a)" msgstr "je n’ai pas pu encoder ~s comme un jeton d’identité (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1133 +#: src/scm/webid-oidc/errors.scm:1175 #, scheme-format msgid "the grant type ~s is not supported" msgstr "le type d’octroi ~s n’est pas supporté " -#: src/scm/webid-oidc/errors.scm:1136 +#: src/scm/webid-oidc/errors.scm:1178 msgid "there is no authorization code in the request" msgstr "il n’y a pas de code d’autorisation dans la requête" -#: src/scm/webid-oidc/errors.scm:1138 +#: src/scm/webid-oidc/errors.scm:1180 msgid "there is no refresh token in the request" msgstr "il n’y a pas de jeton de rafraîchissement dans la requête" -#: src/scm/webid-oidc/errors.scm:1140 +#: src/scm/webid-oidc/errors.scm:1182 #, scheme-format msgid "~s is not an ID token (because ~a)" msgstr "~s n’est pas un jeton d’identité (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1143 +#: src/scm/webid-oidc/errors.scm:1185 #, scheme-format msgid "~s is not an ID token header (because ~a)" msgstr "~s n’est pas un en-tête de jeton d’identité (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1146 +#: src/scm/webid-oidc/errors.scm:1188 #, scheme-format msgid "~s is not an ID token payload (because ~a)" msgstr "~s n’est pas un contenu de jeton d’identité (parce que ~a)" -#: src/scm/webid-oidc/errors.scm:1149 +#: src/scm/webid-oidc/errors.scm:1191 #, scheme-format msgid "" "I couldn’t set the locale to ~s as an approximation of the client locale ~s" @@ -633,71 +633,105 @@ msgstr "" "je n’ai pas pu définir la locale à ~s comme approximation de la locale du " "client ~s" -#: src/scm/webid-oidc/errors.scm:1152 +#: src/scm/webid-oidc/errors.scm:1194 #, scheme-format msgid "~s does not admit ~s as an identity provider" msgstr "~s n’admet pas ~s comme fournisseur d’identité" -#: src/scm/webid-oidc/errors.scm:1157 +#: src/scm/webid-oidc/errors.scm:1197 +#, scheme-format +msgid "" +"~a is neither an identity provider (because ~a) nor a webid (because ~a)" +msgstr "" +"~a n’est ni un fournisseur d’identité (parce que ~a) ni un webid (parce que " +"~a)" + +#: src/scm/webid-oidc/errors.scm:1202 +#, scheme-format +msgid "the token request failed (because ~a)" +msgstr "la requête de jeton a échoué (parce que ~a)" + +#: src/scm/webid-oidc/errors.scm:1205 +#, scheme-format +msgid "you don’t have a refresh token for identity ~a certified by ~a in ~s" +msgstr "" +"vous n’avez pas de jeton de rafraîchissement pour l’identité ~a certifié par " +"~a dans ~s" + +#: src/scm/webid-oidc/errors.scm:1210 +#, scheme-format +msgid "all identity provider candidates for ~a failed: ~a" +msgstr "tous les candidats de fournisseurs d’identité pour ~a ont échoué : ~a" + +#: src/scm/webid-oidc/errors.scm:1214 +#, scheme-format +msgid "~s failed (because ~a)" +msgstr "~s a échoué (parce que ~a)" + +#: src/scm/webid-oidc/errors.scm:1217 +msgid ", " +msgstr ", " + +#: src/scm/webid-oidc/errors.scm:1221 msgid "that’s it" msgstr "c’est tout" -#: src/scm/webid-oidc/errors.scm:1161 +#: src/scm/webid-oidc/errors.scm:1225 #, scheme-format msgid "~a and ~a" msgstr "~a et ~a" -#: src/scm/webid-oidc/errors.scm:1164 +#: src/scm/webid-oidc/errors.scm:1228 #, scheme-format msgid "~a, ~a" msgstr "~a, ~a" -#: src/scm/webid-oidc/errors.scm:1168 +#: src/scm/webid-oidc/errors.scm:1232 #, scheme-format msgid "the signature ~a does not match key ~s with payload ~a" msgstr "la signature ~a ne correspond pas à la clé ~s avec le contenu ~a" -#: src/scm/webid-oidc/errors.scm:1171 +#: src/scm/webid-oidc/errors.scm:1235 msgid "there is an undefined variable" msgstr "il y a une variable non définie" -#: src/scm/webid-oidc/errors.scm:1173 +#: src/scm/webid-oidc/errors.scm:1237 #, scheme-format msgid "the origin is ~a" msgstr "l’origine est ~a" -#: src/scm/webid-oidc/errors.scm:1176 +#: src/scm/webid-oidc/errors.scm:1240 #, scheme-format msgid "a message is attached: ~a" msgstr "un message est attaché : ~a" -#: src/scm/webid-oidc/errors.scm:1179 +#: src/scm/webid-oidc/errors.scm:1243 #, scheme-format msgid "the values ~s are problematic" msgstr "les valeurs ~s sont problématiques" -#: src/scm/webid-oidc/errors.scm:1182 +#: src/scm/webid-oidc/errors.scm:1246 msgid "there is a kind and args" msgstr "il y a un type et des arguments" -#: src/scm/webid-oidc/errors.scm:1184 +#: src/scm/webid-oidc/errors.scm:1248 msgid "there is an assertion failure" msgstr "il y a un échec d’assertion" -#: src/scm/webid-oidc/errors.scm:1186 +#: src/scm/webid-oidc/errors.scm:1250 #, scheme-format msgid "the program quits with code ~a" msgstr "le programme quitte avec le code ~a" -#: src/scm/webid-oidc/errors.scm:1189 +#: src/scm/webid-oidc/errors.scm:1253 msgid "the program cannot recover from this exception" msgstr "le programme ne peut pas récupérer après cette exception" -#: src/scm/webid-oidc/errors.scm:1191 +#: src/scm/webid-oidc/errors.scm:1255 msgid "there is an error" msgstr "il y a une erreur" -#: src/scm/webid-oidc/errors.scm:1193 +#: src/scm/webid-oidc/errors.scm:1257 #, scheme-format msgid "Unhandled exception type ~a." msgstr "Type d’exception non pris en charge ~a." @@ -1273,35 +1307,6 @@ msgstr "" " -p PORT, --~a=8080 :\n" " définit le port à lier.\n" -#, scheme-format -#~ msgid "" -#~ "~a is neither an identity provider (because ~a) nor a webid (because ~a)" -#~ msgstr "" -#~ "~a n’est ni un fournisseur d’identité (parce que ~a) ni un webid (parce " -#~ "que ~a)" - -#, scheme-format -#~ msgid "the token request failed (because ~a)" -#~ msgstr "la requête de jeton a échoué (parce que ~a)" - -#, scheme-format -#~ msgid "you don’t have a refresh token for identity ~a certified by ~a in ~s" -#~ msgstr "" -#~ "vous n’avez pas de jeton de rafraîchissement pour l’identité ~a certifié " -#~ "par ~a dans ~s" - -#, scheme-format -#~ msgid "all identity provider candidates for ~a failed: ~a" -#~ msgstr "" -#~ "tous les candidats de fournisseurs d’identité pour ~a ont échoué : ~a" - -#, scheme-format -#~ msgid "~s failed (because ~a)" -#~ msgstr "~s a échoué (parce que ~a)" - -#~ msgid ", " -#~ msgstr ", " - #, scheme-format #~ msgid "the resource ~s could not be found (because ~a)" #~ msgstr "la ressource ~s n’a pas été trouvée (parce que ~a)" diff --git a/po/webid-oidc.pot b/po/webid-oidc.pot index f3e5c51..2f7d082 100644 --- a/po/webid-oidc.pot +++ b/po/webid-oidc.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: webid-oidc SNAPSHOT\n" "Report-Msgid-Bugs-To: vivien@planete-kraus.eu\n" -"POT-Creation-Date: 2021-06-05 16:20+0200\n" +"POT-Creation-Date: 2021-06-05 16:21+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -122,565 +122,595 @@ msgstr "" msgid "Usage: generate-key [NUMBER OF BITS | CURVE]\n" msgstr "" -#: src/scm/webid-oidc/errors.scm:839 +#: src/scm/webid-oidc/errors.scm:881 msgid "that’s how it is" msgstr "" -#: src/scm/webid-oidc/errors.scm:844 +#: src/scm/webid-oidc/errors.scm:886 #, scheme-format msgid "the value ~s is not a base64 string (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:847 +#: src/scm/webid-oidc/errors.scm:889 #, scheme-format msgid "the value ~s is not JSON (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:850 +#: src/scm/webid-oidc/errors.scm:892 #, scheme-format msgid "the value ~s is not Turtle (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:853 +#: src/scm/webid-oidc/errors.scm:895 #, scheme-format msgid "the value ~s does not identify an elleptic curve" msgstr "" -#: src/scm/webid-oidc/errors.scm:858 +#: src/scm/webid-oidc/errors.scm:900 #, scheme-format msgid "the value ~s does not identify a JWK (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:860 +#: src/scm/webid-oidc/errors.scm:902 #, scheme-format msgid "the value ~s does not identify a JWK" msgstr "" -#: src/scm/webid-oidc/errors.scm:865 +#: src/scm/webid-oidc/errors.scm:907 #, scheme-format msgid "the value ~s does not identify a public JWK (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:867 +#: src/scm/webid-oidc/errors.scm:909 #, scheme-format msgid "the value ~s does not identify a public JWK" msgstr "" -#: src/scm/webid-oidc/errors.scm:872 +#: src/scm/webid-oidc/errors.scm:914 #, scheme-format msgid "the value ~s does not identify a private JWK (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:874 +#: src/scm/webid-oidc/errors.scm:916 #, scheme-format msgid "the value ~s does not identify a private JWK" msgstr "" -#: src/scm/webid-oidc/errors.scm:879 +#: src/scm/webid-oidc/errors.scm:921 #, scheme-format msgid "the value ~s does not identify a JWKS (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:881 +#: src/scm/webid-oidc/errors.scm:923 #, scheme-format msgid "the value ~s does not identify a JWKS" msgstr "" -#: src/scm/webid-oidc/errors.scm:884 +#: src/scm/webid-oidc/errors.scm:926 #, scheme-format msgid "the value ~s does not identify a hash algorithm" msgstr "" -#: src/scm/webid-oidc/errors.scm:887 +#: src/scm/webid-oidc/errors.scm:929 #, scheme-format msgid "the value ~s is not an alist or misses key ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:890 +#: src/scm/webid-oidc/errors.scm:932 #, scheme-format msgid "the value ~s is not a JWS header (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:893 +#: src/scm/webid-oidc/errors.scm:935 #, scheme-format msgid "the value ~s is not a JWS payload (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:896 +#: src/scm/webid-oidc/errors.scm:938 #, scheme-format msgid "the value ~s is not a JWS (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:899 +#: src/scm/webid-oidc/errors.scm:941 #, scheme-format msgid "the string ~s cannot be split in 3 parts with ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:902 +#: src/scm/webid-oidc/errors.scm:944 #, scheme-format msgid "" "all key candidates failed to verify signature ~s with algorithm ~s and " "payload ~a (there were ~a: ~s)" msgstr "" -#: src/scm/webid-oidc/errors.scm:905 +#: src/scm/webid-oidc/errors.scm:947 #, scheme-format msgid "I cannot decode JWS ~a (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:908 +#: src/scm/webid-oidc/errors.scm:950 #, scheme-format msgid "I cannot encode JWS ~a (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:911 +#: src/scm/webid-oidc/errors.scm:953 #, scheme-format msgid "" "the server request unexpectedly failed with code ~a and reason phrase ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:916 +#: src/scm/webid-oidc/errors.scm:958 #, scheme-format msgid "the header ~a should not have the value ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:918 +#: src/scm/webid-oidc/errors.scm:960 #, scheme-format msgid "the header ~a should be present" msgstr "" -#: src/scm/webid-oidc/errors.scm:921 +#: src/scm/webid-oidc/errors.scm:963 #, scheme-format msgid "the server response wasn't expected: ~s (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:927 +#: src/scm/webid-oidc/errors.scm:969 #, scheme-format msgid "the value ~s is not an OIDC configuration (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:932 +#: src/scm/webid-oidc/errors.scm:974 #, scheme-format msgid "the webid field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:933 +#: src/scm/webid-oidc/errors.scm:975 msgid "the webid field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:937 +#: src/scm/webid-oidc/errors.scm:979 #, scheme-format msgid "the sub field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:938 +#: src/scm/webid-oidc/errors.scm:980 msgid "the sub field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:942 +#: src/scm/webid-oidc/errors.scm:984 #, scheme-format msgid "the iss field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:943 +#: src/scm/webid-oidc/errors.scm:985 msgid "the iss field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:947 +#: src/scm/webid-oidc/errors.scm:989 #, scheme-format msgid "the aud field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:948 +#: src/scm/webid-oidc/errors.scm:990 msgid "the aud field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:952 +#: src/scm/webid-oidc/errors.scm:994 #, scheme-format msgid "the iat field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:953 +#: src/scm/webid-oidc/errors.scm:995 msgid "the iat field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:957 +#: src/scm/webid-oidc/errors.scm:999 #, scheme-format msgid "the exp field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:958 +#: src/scm/webid-oidc/errors.scm:1000 msgid "the exp field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:962 +#: src/scm/webid-oidc/errors.scm:1004 #, scheme-format msgid "the cnf/jkt field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:963 +#: src/scm/webid-oidc/errors.scm:1005 msgid "the cnf/jkt field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:967 +#: src/scm/webid-oidc/errors.scm:1009 #, scheme-format msgid "the client-id field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:968 +#: src/scm/webid-oidc/errors.scm:1010 msgid "the client-id field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:972 +#: src/scm/webid-oidc/errors.scm:1014 #: src/scm/webid-oidc/authorization-page-unsafe.scm:133 #, scheme-format msgid "the redirect_uris field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:973 +#: src/scm/webid-oidc/errors.scm:1015 #: src/scm/webid-oidc/authorization-page-unsafe.scm:134 msgid "the redirect_uris field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:977 +#: src/scm/webid-oidc/errors.scm:1019 #, scheme-format msgid "the typ field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:978 +#: src/scm/webid-oidc/errors.scm:1020 msgid "the typ field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:982 +#: src/scm/webid-oidc/errors.scm:1024 #, scheme-format msgid "the jwk field is incorrect: ~s (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:984 +#: src/scm/webid-oidc/errors.scm:1026 msgid "the jwk field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:988 +#: src/scm/webid-oidc/errors.scm:1030 #, scheme-format msgid "the jti field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:989 +#: src/scm/webid-oidc/errors.scm:1031 msgid "the jti field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:993 +#: src/scm/webid-oidc/errors.scm:1035 #, scheme-format msgid "the nonce field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:994 +#: src/scm/webid-oidc/errors.scm:1036 msgid "the nonce field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:998 +#: src/scm/webid-oidc/errors.scm:1040 #, scheme-format msgid "the htm field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:999 +#: src/scm/webid-oidc/errors.scm:1041 msgid "the htm field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:1003 +#: src/scm/webid-oidc/errors.scm:1045 #, scheme-format msgid "the htu field is incorrect: ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:1004 +#: src/scm/webid-oidc/errors.scm:1046 msgid "the htu field is missing" msgstr "" -#: src/scm/webid-oidc/errors.scm:1006 +#: src/scm/webid-oidc/errors.scm:1048 #, scheme-format msgid "~s is not an access token (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1009 +#: src/scm/webid-oidc/errors.scm:1051 #, scheme-format msgid "~s is not an access token header (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1012 +#: src/scm/webid-oidc/errors.scm:1054 #, scheme-format msgid "~s is not an access token payload (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1015 +#: src/scm/webid-oidc/errors.scm:1057 #, scheme-format msgid "~s is not a DPoP proof (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1018 +#: src/scm/webid-oidc/errors.scm:1060 #, scheme-format msgid "~s is not a DPoP proof header (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1021 +#: src/scm/webid-oidc/errors.scm:1063 #, scheme-format msgid "~s is not a DPoP proof payload (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1024 +#: src/scm/webid-oidc/errors.scm:1066 #, scheme-format msgid "I cannot fetch the issuer configuration of ~a (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1031 +#: src/scm/webid-oidc/errors.scm:1073 #, scheme-format msgid "I cannot fetch the JWKS of ~a at ~a (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1042 +#: src/scm/webid-oidc/errors.scm:1084 #, scheme-format msgid "the HTTP method is signed for ~s, but ~s was requested" msgstr "" -#: src/scm/webid-oidc/errors.scm:1045 +#: src/scm/webid-oidc/errors.scm:1087 #, scheme-format msgid "the HTTP uri is signed for ~a, but ~a was requested" msgstr "" -#: src/scm/webid-oidc/errors.scm:1048 +#: src/scm/webid-oidc/errors.scm:1090 #, scheme-format msgid "the date is ~a, but the DPoP proof is signed in the future at ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1052 +#: src/scm/webid-oidc/errors.scm:1094 #, scheme-format msgid "the date is ~a, but the DPoP proof was signed too long ago at ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1061 +#: src/scm/webid-oidc/errors.scm:1103 #, scheme-format msgid "the key ~s does not hash to ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1063 +#: src/scm/webid-oidc/errors.scm:1105 #, scheme-format msgid "the key confirmation of ~s failed (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1065 +#: src/scm/webid-oidc/errors.scm:1107 #, scheme-format msgid "the key confirmation of ~s failed" msgstr "" -#: src/scm/webid-oidc/errors.scm:1067 +#: src/scm/webid-oidc/errors.scm:1109 #, scheme-format msgid "the jti ~s has already been found (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1070 +#: src/scm/webid-oidc/errors.scm:1112 #, scheme-format msgid "I cannot decode ~s as an access token (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1073 +#: src/scm/webid-oidc/errors.scm:1115 #, scheme-format msgid "I cannot encode ~s as an access token with key ~s (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1076 +#: src/scm/webid-oidc/errors.scm:1118 #, scheme-format msgid "I cannot decode ~s as a DPoP proof (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1079 +#: src/scm/webid-oidc/errors.scm:1121 #, scheme-format msgid "I cannot encode ~s as a DPoP proof (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1082 +#: src/scm/webid-oidc/errors.scm:1124 #, scheme-format msgid "I could not fetch a RDF graph at ~a (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1085 +#: src/scm/webid-oidc/errors.scm:1127 #, scheme-format msgid "~s is not a client manifest (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1088 +#: src/scm/webid-oidc/errors.scm:1130 #, scheme-format msgid "~s does not authorize redirection URI ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1091 +#: src/scm/webid-oidc/errors.scm:1133 msgid "I cannot serve a public manifest" msgstr "" -#: src/scm/webid-oidc/errors.scm:1093 +#: src/scm/webid-oidc/errors.scm:1135 #, scheme-format msgid "~a does not have a client manifest registration triple" msgstr "" -#: src/scm/webid-oidc/errors.scm:1096 +#: src/scm/webid-oidc/errors.scm:1138 #, scheme-format msgid "the client manifest at ~a is advertised for ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1099 +#: src/scm/webid-oidc/errors.scm:1141 #, scheme-format msgid "I could not fetch the client manifest of ~a (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1102 +#: src/scm/webid-oidc/errors.scm:1144 #, scheme-format msgid "~s is not an authorization code (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1105 +#: src/scm/webid-oidc/errors.scm:1147 #, scheme-format msgid "~s is not an authorization code header (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1108 +#: src/scm/webid-oidc/errors.scm:1150 #, scheme-format msgid "~s is not an authorization code payload (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1111 +#: src/scm/webid-oidc/errors.scm:1153 #, scheme-format msgid "the current time is ~a, and the authorization code expired at ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1115 +#: src/scm/webid-oidc/errors.scm:1157 #, scheme-format msgid "I cannot decode ~s as an authorization code (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1118 +#: src/scm/webid-oidc/errors.scm:1160 #, scheme-format msgid "I cannot encode ~s as an authorization code (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1121 +#: src/scm/webid-oidc/errors.scm:1163 #, scheme-format msgid "there is no such refresh token as ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:1124 +#: src/scm/webid-oidc/errors.scm:1166 #, scheme-format msgid "" "the refresh token is bound to a key confirmed as ~s, but it is used with key " "~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:1127 +#: src/scm/webid-oidc/errors.scm:1169 #, scheme-format msgid "I cannot decode ~s as an ID token (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1130 +#: src/scm/webid-oidc/errors.scm:1172 #, scheme-format msgid "I cannot encode ~s as an ID token (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1133 +#: src/scm/webid-oidc/errors.scm:1175 #, scheme-format msgid "the grant type ~s is not supported" msgstr "" -#: src/scm/webid-oidc/errors.scm:1136 +#: src/scm/webid-oidc/errors.scm:1178 msgid "there is no authorization code in the request" msgstr "" -#: src/scm/webid-oidc/errors.scm:1138 +#: src/scm/webid-oidc/errors.scm:1180 msgid "there is no refresh token in the request" msgstr "" -#: src/scm/webid-oidc/errors.scm:1140 +#: src/scm/webid-oidc/errors.scm:1182 #, scheme-format msgid "~s is not an ID token (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1143 +#: src/scm/webid-oidc/errors.scm:1185 #, scheme-format msgid "~s is not an ID token header (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1146 +#: src/scm/webid-oidc/errors.scm:1188 #, scheme-format msgid "~s is not an ID token payload (because ~a)" msgstr "" -#: src/scm/webid-oidc/errors.scm:1149 +#: src/scm/webid-oidc/errors.scm:1191 #, scheme-format msgid "" "I couldn’t set the locale to ~s as an approximation of the client locale ~s" msgstr "" -#: src/scm/webid-oidc/errors.scm:1152 +#: src/scm/webid-oidc/errors.scm:1194 #, scheme-format msgid "~s does not admit ~s as an identity provider" msgstr "" -#: src/scm/webid-oidc/errors.scm:1157 +#: src/scm/webid-oidc/errors.scm:1197 +#, scheme-format +msgid "" +"~a is neither an identity provider (because ~a) nor a webid (because ~a)" +msgstr "" + +#: src/scm/webid-oidc/errors.scm:1202 +#, scheme-format +msgid "the token request failed (because ~a)" +msgstr "" + +#: src/scm/webid-oidc/errors.scm:1205 +#, scheme-format +msgid "you don’t have a refresh token for identity ~a certified by ~a in ~s" +msgstr "" + +#: src/scm/webid-oidc/errors.scm:1210 +#, scheme-format +msgid "all identity provider candidates for ~a failed: ~a" +msgstr "" + +#: src/scm/webid-oidc/errors.scm:1214 +#, scheme-format +msgid "~s failed (because ~a)" +msgstr "" + +#: src/scm/webid-oidc/errors.scm:1217 +msgid ", " +msgstr "" + +#: src/scm/webid-oidc/errors.scm:1221 msgid "that’s it" msgstr "" -#: src/scm/webid-oidc/errors.scm:1161 +#: src/scm/webid-oidc/errors.scm:1225 #, scheme-format msgid "~a and ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1164 +#: src/scm/webid-oidc/errors.scm:1228 #, scheme-format msgid "~a, ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1168 +#: src/scm/webid-oidc/errors.scm:1232 #, scheme-format msgid "the signature ~a does not match key ~s with payload ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1171 +#: src/scm/webid-oidc/errors.scm:1235 msgid "there is an undefined variable" msgstr "" -#: src/scm/webid-oidc/errors.scm:1173 +#: src/scm/webid-oidc/errors.scm:1237 #, scheme-format msgid "the origin is ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1176 +#: src/scm/webid-oidc/errors.scm:1240 #, scheme-format msgid "a message is attached: ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1179 +#: src/scm/webid-oidc/errors.scm:1243 #, scheme-format msgid "the values ~s are problematic" msgstr "" -#: src/scm/webid-oidc/errors.scm:1182 +#: src/scm/webid-oidc/errors.scm:1246 msgid "there is a kind and args" msgstr "" -#: src/scm/webid-oidc/errors.scm:1184 +#: src/scm/webid-oidc/errors.scm:1248 msgid "there is an assertion failure" msgstr "" -#: src/scm/webid-oidc/errors.scm:1186 +#: src/scm/webid-oidc/errors.scm:1250 #, scheme-format msgid "the program quits with code ~a" msgstr "" -#: src/scm/webid-oidc/errors.scm:1189 +#: src/scm/webid-oidc/errors.scm:1253 msgid "the program cannot recover from this exception" msgstr "" -#: src/scm/webid-oidc/errors.scm:1191 +#: src/scm/webid-oidc/errors.scm:1255 msgid "there is an error" msgstr "" -#: src/scm/webid-oidc/errors.scm:1193 +#: src/scm/webid-oidc/errors.scm:1257 #, scheme-format msgid "Unhandled exception type ~a." msgstr "" diff --git a/src/scm/webid-oidc/Makefile.am b/src/scm/webid-oidc/Makefile.am index 8f5f105..1f9cb5d 100644 --- a/src/scm/webid-oidc/Makefile.am +++ b/src/scm/webid-oidc/Makefile.am @@ -22,7 +22,8 @@ dist_webidoidcmod_DATA += \ %reldir%/provider-confirmation.scm \ %reldir%/resource-server.scm \ %reldir%/hello-world.scm \ - %reldir%/reverse-proxy.scm + %reldir%/reverse-proxy.scm \ + %reldir%/client.scm webidoidcgo_DATA += \ %reldir%/errors.go \ @@ -48,6 +49,7 @@ webidoidcgo_DATA += \ %reldir%/provider-confirmation.go \ %reldir%/resource-server.go \ %reldir%/hello-world.go \ - %reldir%/reverse-proxy.go + %reldir%/reverse-proxy.go \ + %reldir%/client.go EXTRA_DIST += %reldir%/ChangeLog diff --git a/src/scm/webid-oidc/cache.scm b/src/scm/webid-oidc/cache.scm index 9d9da0f..9435c10 100644 --- a/src/scm/webid-oidc/cache.scm +++ b/src/scm/webid-oidc/cache.scm @@ -35,7 +35,7 @@ (define (default-cache-dir) (let ((xdg-cache-home (or (getenv "XDG_CACHE_HOME") - (format #f "~a/.cache")))) + (format #f "~a/.cache" (getenv "HOME"))))) (format #f "~a/webid-oidc" xdg-cache-home))) (define (web-cache-dir dir) diff --git a/src/scm/webid-oidc/client.scm b/src/scm/webid-oidc/client.scm new file mode 100644 index 0000000..ef0d116 --- /dev/null +++ b/src/scm/webid-oidc/client.scm @@ -0,0 +1,488 @@ +(define-module (webid-oidc client) + #:use-module (webid-oidc errors) + #:use-module (webid-oidc provider-confirmation) + #:use-module (webid-oidc oidc-configuration) + #:use-module (webid-oidc oidc-id-token) + #:use-module (webid-oidc dpop-proof) + #:use-module (webid-oidc jwk) + #:use-module ((webid-oidc stubs) #:prefix stubs:) + #:use-module ((webid-oidc refresh-token) #:prefix refresh:) + #:use-module (web uri) + #:use-module (web client) + #:use-module (web response) + #:use-module (web server) + #:use-module (web http) + #:use-module (ice-9 optargs) + #:use-module (ice-9 receive) + #:use-module (srfi srfi-19) + #:use-module (rnrs bytevectors)) + +(define*-public (authorize host-or-webid + #:key + (client-id #f) + (redirect-uri #f) + (state #f) + (http-get http-get)) + (define cannot-be-webid #f) + (define candidate-errors '()) + ;; host-or-webid can be: the host (as a string), an URI (as a string + ;; or an URI). 3 differents things. + (when (string? host-or-webid) + ;; If it’s a string, it can be either a host name or a URI. + (set! host-or-webid + (catch #t + (lambda () + (let ((urified (string->uri host-or-webid))) + (if urified + urified + (error "It’s not a string representing an URI.")))) + (lambda error + (build-uri 'https #:host host-or-webid))))) + ;; client-id and redirect-uri are required, state must be a string. + (when (string? client-id) + (set! client-id (string->uri client-id))) + (when (string? redirect-uri) + (set! redirect-uri (string->uri redirect-uri))) + (let ((host-candidates + (with-exception-handler + (lambda (why-not-webid) + ;; try as an identity provider + (set! cannot-be-webid why-not-webid) + (build-uri 'https + #:userinfo (uri-userinfo host-or-webid) + #:host (uri-host host-or-webid) + #:port (uri-port host-or-webid))) + (lambda () + (get-provider-confirmations host-or-webid #:http-get http-get)) + #:unwind? #t))) + (let ((configurations + (if cannot-be-webid + (with-exception-handler + (lambda (why-not-identity-provider) + (raise-neither-identity-provider-nor-webid + host-or-webid + why-not-identity-provider + cannot-be-webid)) + (lambda () + (cons (uri->string host-candidates) + (get-oidc-configuration (uri-host host-candidates) + #:userinfo (uri-userinfo host-candidates) + #:port (uri-port host-candidates) + #:http-get http-get)))) + (filter + (lambda (cfg) cfg) + (map + (lambda (host) + (with-exception-handler + (lambda (cause) + (set! candidate-errors (acons host cause candidate-errors)) + #f) + (lambda () + (cons (uri->string host) + (get-oidc-configuration (uri-host host) + #:userinfo (uri-userinfo host) + #:port (uri-port host) + #:http-get http-get))) + #:unwind? #t)) + host-candidates))))) + (let ((authorization-endpoints + (if cannot-be-webid + (with-exception-handler + (lambda (why-not-identity-provider) + (raise-neither-identity-provider-nor-webid + host-or-webid + why-not-identity-provider + cannot-be-webid)) + (lambda () + (let ((host (car configurations)) + (cfg (cdr configurations))) + (cons host (oidc-configuration-authorization-endpoint cfg))))) + (map + (lambda (host/cfg) + (let ((host (car host/cfg)) + (cfg (cdr host/cfg))) + (with-exception-handler + (lambda (cause) + (set! candidate-errors (acons (string->uri host) cause + candidate-errors))) + (lambda () + (cons host + (oidc-configuration-authorization-endpoint cfg))) + #:unwind? #t))) + configurations)))) + (if cannot-be-webid + (let ((host (car authorization-endpoints)) + (authz (cdr authorization-endpoints))) + (list + (cons + host + (build-uri (uri-scheme authz) + #:userinfo (uri-userinfo authz) + #:host (uri-host authz) + #:port (uri-port authz) + #:path (uri-path authz) + #:query (format #f "client_id=~a&redirect_uri=~a~a" + (uri-encode (uri->string client-id)) + (uri-encode (uri->string redirect-uri)) + (if state + (format #f "&state=~a" + (uri-encode state)) + "")))))) + (let ((final-candidates + (map + (lambda (host/authorization-endpoint) + (let ((host (car host/authorization-endpoint)) + (authorization-endpoint (cdr host/authorization-endpoint))) + (cons + host + (build-uri (uri-scheme authorization-endpoint) + #:userinfo (uri-userinfo authorization-endpoint) + #:host (uri-host authorization-endpoint) + #:port (uri-port authorization-endpoint) + #:path (uri-path authorization-endpoint) + #:query (format #f "client_id=~a&redirect_uri=~a~a" + (uri-encode (uri->string client-id)) + (uri-encode (uri->string redirect-uri)) + (if state + (format #f "&state=~a" + (uri-encode state)) + "")))))) + authorization-endpoints))) + (when (null? final-candidates) + (raise-no-provider-candidates host-or-webid candidate-errors)) + final-candidates)))))) + +(define the-current-time current-time) + +(define*-public (token host client-key + #:key + (authorization-code #f) + (refresh-token #f) + (http-get http-get) + (http-post http-post) + (current-time #f)) + (unless (or authorization-code refresh-token) + (scm-error 'wrong-type-arg "token" + "You need to either set #:authorization-code or #:refresh-token." + '() + (list authorization-code))) + (unless current-time + (set! current-time the-current-time)) + (when (thunk? current-time) + (set! current-time (current-time))) + (when (integer? current-time) + (set! current-time (make-time time-utc 0 current-time))) + (when (time? current-time) + (set! current-time (time-utc->date current-time))) + (let ((token-endpoint + (oidc-configuration-token-endpoint + (get-oidc-configuration host #:http-get http-get))) + (grant-type + (if authorization-code + "authorization_code" + "refresh_token"))) + (let ((dpop-proof + (issue-dpop-proof + client-key + #:alg (case (kty client-key) + ((EC) 'ES256) + ((RSA) 'RS256) + (else + (error "Unknown key type of ~S." client-key))) + #:htm 'POST + #:htu token-endpoint + #:iat current-time))) + (receive (response response-body) + (http-post token-endpoint + #:body + (string-join + (map + (lambda (arg) + (string-append (uri-encode (car arg)) + "=" + (uri-encode (cdr arg)))) + `(("grant_type" . ,grant-type) + ,@(if authorization-code + `(("code" . ,authorization-code)) + '()) + ,@(if refresh-token + `(("refresh_token" . ,refresh-token)) + '()))) + "&") + #:headers + `((content-type application/x-www-form-urlencoded) + (dpop . ,dpop-proof))) + (with-exception-handler + (lambda (error) + (raise-token-request-failed error)) + (lambda () + (when (bytevector? response-body) + (set! response-body (utf8->string response-body))) + (with-exception-handler + (lambda (error) + (raise-unexpected-response response error)) + (lambda () + (unless (eqv? (response-code response) 200) + (raise-request-failed-unexpectedly + (response-code response) + (response-reason-phrase response))) + (unless (and (response-content-type response) + (eq? (car (response-content-type response 'application/json)))) + (raise-unexpected-header-value 'content-type (response-content-type response))) + (stubs:json-string->scm response-body))))))))) + +(define (default-dir) + (let ((xdg-data-home + (or + (getenv "XDG_DATA_HOME") + (format #f "~a/.local/share" + (getenv "HOME"))))) + (format #f "~a/webid-oidc" xdg-data-home))) + +(define*-public (list-profiles #:key (dir default-dir)) + (when (thunk? dir) + (set! dir (dir))) + (map (lambda (profile) + (list + (string->uri (car profile)) ;; webid + (string->uri (cadr profile)) ;; issuer + (caddr profile) ;; refresh token + (cadddr profile))) ;; key + (catch #t + (lambda () + (call-with-input-file (string-append dir "/profiles") + read)) + (lambda error + (format (current-error-port) "Could not read profiles: ~s\n" error) + '())))) + +(define* (add-profile webid issuer refresh-token key + #:key (dir default-dir)) + (when (thunk? dir) + (set! dir (dir))) + (let ((other-profiles (list-profiles #:dir dir))) + (stubs:atomically-update-file + (string-append dir "/profiles") + (string-append dir "/profiles.lock") + (lambda (port) + (write + (map (lambda (profile) + (list + (uri->string (car profile)) ;; webid + (uri->string (cadr profile)) ;; issuer + (caddr profile) ;; refresh token + key)) ;; key + (cons `(,webid + ,issuer + ,refresh-token) + other-profiles)) + port))))) + +(define*-public (setup get-host/webid choose-provider browse-authorization-uri + #:key + (client-id #f) + (redirect-uri #f) + (dir default-dir) + (http-get http-get) + (http-post http-post) + (current-time #f)) + (when (thunk? dir) + (set! dir (dir))) + (let ((host/webid (get-host/webid))) + (let ((authorization-uris + (authorize host/webid + #:client-id client-id + #:redirect-uri redirect-uri + #:http-get http-get)) + (key (generate-key #:n-size 2048))) + (let ((provider (choose-provider (map car authorization-uris)))) + (let ((authz-uri (assq-ref authorization-uris provider))) + (let ((authz-code (browse-authorization-uri authz-uri))) + (let ((params + (token host/webid key + #:authorization-code authz-code + #:http-get http-get + #:http-post http-post + #:current-time current-time))) + (let ((id-token (id-token-decode (assq-ref params 'id_token) + #:http-get http-get)) + (access-token (assq-ref params 'access_token)) + (refresh-token (assq-ref params 'refresh_token))) + (when refresh-token + ;; Save it to disk + (add-profile (id-token-webid id-token) + (id-token-iss id-token) + refresh-token + key + #:dir dir)) + (values (cdr id-token) access-token key))))))))) + +(define*-public (login webid issuer refresh-token key + #:key + (dir default-dir) + (http-get http-get) + (http-post http-post) + (current-time #f)) + (when (string? webid) + (set! webid (string->uri webid))) + (when (string? issuer) + (set! issuer (string->uri issuer))) + (let ((iss-host (uri-host issuer))) + (let ((params + (token iss-host key + #:refresh-token refresh-token + #:http-get http-get + #:http-post http-post + #:current-time current-time))) + (let ((id-token (id-token-decode (assq-ref params 'id_token) + #:http-get http-get)) + (access-token (assq-ref params 'access_token)) + (new-refresh-token (assq-ref params 'refresh-token))) + (when (and new-refresh-token + (not (equal? refresh-token new-refresh-token))) + ;; The refresh token has been updated + (add-profile (id-token-webid id-token) + (id-token-iss id-token) + refresh-token + key + #:dir dir)) + (values (cdr id-token) access-token key))))) + +(define*-public (refresh id-token + key + #:key + (dir default-dir) + (http-get http-get) + (http-post http-post) + (current-time #f)) + (when (thunk? dir) + (set! dir (dir))) + (when (id-token-payload? id-token) + ;; For convenience, we’d like a full ID token to use the ID token + ;; API. + (set! id-token (cons `((alg . "HS256")) id-token))) + (let ((profiles (list-profiles #:dir dir))) + (letrec ((find-refresh-token + (lambda (profiles) + (when (null? profiles) + (raise-profile-not-found (id-token-webid id-token) + (id-token-iss id-token) + dir)) + (let ((prof (car profiles)) + (others (cdr profiles))) + (let ((webid (car prof)) + (issuer (cadr prof)) + (refresh (caddr prof))) + (if (and (equal? webid (id-token-webid id-token)) + (equal? issuer (id-token-iss id-token))) + refresh + (find-refresh-token others))))))) + (login (id-token-webid id-token) + (id-token-iss id-token) + (find-refresh-token (profiles)) + key + #:dir dir + #:http-get http-get + #:http-post http-post + #:current-time current-time)))) + +(define* (renew-if-expired id-token access-token key + date + #:key + (dir default-dir) + (http-get http-get) + (http-post http-post)) + ;; Since we’re not supposed to decode the access token, we’re + ;; judging from the ID token to know if it has expired. + (when (date? date) + (set! date (date->time-utc date))) + (when (time? date) + (set! date (time-second date))) + (when (id-token-payload? id-token) + ;; See the refresh function + (set! id-token (cons `((alg . "HS256")) id-token))) + (let ((exp (id-token-exp id-token))) + (set! exp (date->time-utc exp)) + (set! exp (time-second exp)) + (if (>= date exp) + (refresh id-token key + #:dir dir + #:http-get http-get + #:http-post http-post + #:current-time date) + (values id-token access-token key)))) + +(define*-public (make-client id-token access-token key + #:key + (dir default-dir) + (http-get http-get) + (http-post http-post) + (http-request http-request) + (current-time the-current-time)) + ;; HACK: guile does not support other authentication schemes in + ;; WWW-Authenticate than Basic, so it will crash when a response + ;; containing that header will be issued. + (declare-header! "WWW-Authenticate" string->symbol symbol? write) + (define (handler uri method headers other-args current-time retry?) + (let ((proof (issue-dpop-proof + key + #:alg (case (kty key) + ((EC) 'ES256) + ((RSA) 'RS256) + (else + (error "Unknown key type of ~S." key))) + #:htm method + #:htu uri + #:iat current-time))) + (receive (response response-body) + (apply http-request uri + #:method method + #:headers (append `((dpop . ,proof) + (Authorization . ,(string-append "DPoP " access-token))) + headers) + other-args) + (let ((server-date (response-date response)) + (code (response-code response))) + (if (and retry? (eqv? code 401)) + ;; Maybe the access token has expired? + (receive (new-id-token new-access-token new-key) + (renew-if-expired id-token access-token key server-date + #:dir dir + #:http-get http-get + #:http-post http-post) + (if (equal? access-token new-access-token) + ;; No, it’s just that way. + (values response response-body) + ;; Ah, we have a new access token + (begin + (set! id-token new-id-token) + (set! access-token new-access-token) + (set! key new-key) + (handler uri method headers other-args current-time #f)))) + (values response response-body)))))) + (define (parse-args uri method headers other-args-rev rest) + (if (null? rest) + (let ((the-current-time current-time)) + (when (thunk? the-current-time) + (set! the-current-time (the-current-time))) + (when (integer? the-current-time) + (set! the-current-time (make-time time-utc 0 the-current-time))) + (when (time? the-current-time) + (set! the-current-time (time-utc->date the-current-time))) + (handler uri method headers (reverse other-args-rev) the-current-time #t)) + (let ((kw (car rest))) + (case kw + ((#:method) + (if (null? (cdr rest)) + (parse-args uri method headers (cons kw other-args-rev) '()) + (parse-args uri (cadr rest) headers other-args-rev (cddr rest)))) + ((#:headers) + (if (null? (cdr rest)) + (parse-args uri method headers (cons kw other-args-rev) '()) + (parse-args uri method (append headers (cadr rest)) other-args-rev (cddr rest)))) + (else + (parse-args uri method headers (cons kw other-args-rev) '())))))) + (define (parse-http-request-args uri args) + (parse-args uri 'GET '() '() args)) + (lambda (uri . args) + (parse-http-request-args uri args))) diff --git a/src/scm/webid-oidc/errors.scm b/src/scm/webid-oidc/errors.scm index 4a62abb..52f5db8 100644 --- a/src/scm/webid-oidc/errors.scm +++ b/src/scm/webid-oidc/errors.scm @@ -455,9 +455,10 @@ &external-error '(issuer cause))) -(define-public (raise-cannot-fetch-issuer-configuration issuer cause) +(define*-public (raise-cannot-fetch-issuer-configuration issuer cause #:key (recoverable? #f)) (raise-exception - ((record-constructor &cannot-fetch-issuer-configuration) issuer cause))) + ((record-constructor &cannot-fetch-issuer-configuration) issuer cause) + #:continuable? recoverable?)) (define-public &cannot-fetch-jwks (make-exception-type @@ -828,6 +829,47 @@ (raise-exception ((record-constructor &unconfirmed-provider) subject provider))) +(define-public &neither-identity-provider-nor-webid + (make-exception-type + '&neither-identity-provider-nor-webid + &external-error + '(uri why-not-identity-provider why-not-webid))) + +(define-public (raise-neither-identity-provider-nor-webid uri why-not-identity-provider why-not-webid) + (raise-exception + ((record-constructor &neither-identity-provider-nor-webid) + uri why-not-identity-provider why-not-webid))) + +(define-public &token-request-failed + (make-exception-type + '&token-request-failed + &external-error + '(cause))) + +(define-public (raise-token-request-failed cause) + (raise-exception + ((record-constructor &token-request-failed) cause))) + +(define-public &profile-not-found + (make-exception-type + '&profile-not-found + &external-error + '(webid iss dir))) + +(define-public (raise-profile-not-found webid iss dir) + (raise-exception + ((record-constructor &profile-not-found) webid iss dir))) + +(define-public &no-provider-candidates + (make-exception-type + '&no-provider-candidates + &external-error + '(webid causes))) + +(define-public (raise-no-provider-candidates webid causes) + (raise-exception + ((record-constructor &no-provider-candidates) webid causes))) + (define*-public (error->str err #:key (max-depth #f)) (if (record? err) (let* ((type (record-type-descriptor err)) @@ -1151,6 +1193,28 @@ ((&unconfirmed-provider) (format #f (G_ "~s does not admit ~s as an identity provider") (get 'subject) (get 'provider))) + ((&neither-identity-provider-nor-webid) + (format #f (G_ "~a is neither an identity provider (because ~a) nor a webid (because ~a)") + (uri->string (get 'uri)) + (recurse (get 'why-not-identity-provider)) + (recurse (get 'why-not-webid)))) + ((&token-request-failed) + (format #f (G_ "the token request failed (because ~a)") + (recurse (get 'cause)))) + ((&profile-not-found) + (format #f (G_ "you don’t have a refresh token for identity ~a certified by ~a in ~s") + (uri->string (get 'webid)) + (uri->string (get 'iss)) + (get 'dir))) + ((&no-provider-candidates) + (format #f (G_ "all identity provider candidates for ~a failed: ~a") + (uri->string (get 'webid)) + (string-join + (map (lambda (cause) + (format #f (G_ "~s failed (because ~a)") + (uri->string (car cause)) (recurse (cdr cause)))) + (get 'causes)) + (G_ ", ")))) ((&compound-exception) (let ((components (get 'components))) (if (null? components) diff --git a/tests/Makefile.am b/tests/Makefile.am index 6d0df35..52a0083 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -37,7 +37,9 @@ TESTS = %reldir%/load-library.scm \ %reldir%/token-endpoint-issue.scm \ %reldir%/token-endpoint-refresh.scm \ %reldir%/provider-confirmation.scm \ - %reldir%/resource-server.scm + %reldir%/resource-server.scm \ + %reldir%/client-authorization.scm \ + %reldir%/client-token.scm EXTRA_DIST += $(TESTS) %reldir%/ChangeLog diff --git a/tests/client-authorization.scm b/tests/client-authorization.scm new file mode 100644 index 0000000..ed02edf --- /dev/null +++ b/tests/client-authorization.scm @@ -0,0 +1,118 @@ +(use-modules (webid-oidc client) + (webid-oidc testing) + ((webid-oidc stubs) #:prefix stubs:) + (web uri) + (web response) + (srfi srfi-19) + (ice-9 optargs) + (ice-9 receive) + (ice-9 hash-table)) + +;; We need to test different things. + +;; 1. It works when passed a host +;; 2. It works when passed a webid with foreign identity providers +;; 3. It works when passed a webid without foreign identity providers + +(with-test-environment + "client-authorization" + (lambda () + (define* (http-get uri #:key (headers '())) + (cond + ;; 1. We pass a host name + ((equal? uri (string->uri "https://case-1.client-authorization.scm/.well-known/openid-configuration")) + (values + (build-response #:headers `((content-type application/json))) + (stubs:scm->json-string + `((jwks_uri . "https://case-1.client-authorization.scm/keys") + (authorization_endpoint . "https://case-1.client-authorization.scm/authorize") + (token_endpoint . "https://case-1.client-authorization.scm/token"))))) + ;; It’s not a webid + ((equal? uri (string->uri "https://case-1.client-authorization.scm")) + (values + (build-response #:code 404 #:reason-phrase "Not Found") + #f)) + ;; 2. We first dereference the webid + ((equal? uri (string->uri "https://case-2.client-authorization.scm/profile/card#me")) + (values + (build-response #:headers `((content-type text/turtle))) + "<#me> , .")) + ;; and we get the config of all IPs + ((equal? uri (string->uri "https://case-2.client-authorization.scm/.well-known/openid-configuration")) + (values + (build-response #:headers `((content-type application/json))) + (stubs:scm->json-string + `((jwks_uri . "https://case-2.client-authorization.scm/keys") + (authorization_endpoint . "https://case-2.client-authorization.scm/authorize") + (token_endpoint . "https://case-2.client-authorization.scm/token"))))) + ((equal? uri (string->uri "https://one.identity.provider/.well-known/openid-configuration")) + (values + (build-response #:headers `((content-type application/json))) + (stubs:scm->json-string + `((jwks_uri . "https://one.identity.provider/keys") + (authorization_endpoint . "https://one.identity.provider/authorize") + (token_endpoint . "https://one.identity.provider/token"))))) + ((equal? uri (string->uri "https://another.identity.provider/.well-known/openid-configuration")) + (values + (build-response #:headers `((content-type application/json))) + (stubs:scm->json-string + `((jwks_uri . "https://another.identity.provider/keys") + (authorization_endpoint . "https://another.identity.provider/authorize") + (token_endpoint . "https://another.identity.provider/token"))))) + ;; 3. The webid has no IPs. + ((equal? uri (string->uri "https://case-3.client-authorization.scm/profile/card#me")) + (values + (build-response #:headers `((content-type text/turtle))) + "")) + ;; so we query the host of the webid. + ((equal? uri (string->uri "https://case-3.client-authorization.scm/.well-known/openid-configuration")) + (values + (build-response #:headers `((content-type application/json))) + (stubs:scm->json-string + `((jwks_uri . "https://case-3.client-authorization.scm/keys") + (authorization_endpoint . "https://case-3.client-authorization.scm/authorize") + (token_endpoint . "https://case-3.client-authorization.scm/token"))))) + (else + (format (current-error-port) "Unexpected GET query of URI ~a.\n" (uri->string uri)) + (exit 1)))) + (let ((case-1 (authorize "case-1.client-authorization.scm" + #:client-id "https://app.client-authorization.scm" + #:redirect-uri "https://app.client-authorization.scm/redirected" + #:state "integrity&check" + #:http-get http-get)) + (case-2 (authorize "https://case-2.client-authorization.scm/profile/card#me" + #:client-id "https://app.client-authorization.scm" + #:redirect-uri "https://app.client-authorization.scm/redirected" + #:state "integrity&check" + #:http-get http-get)) + (case-3 (authorize "https://case-3.client-authorization.scm/profile/card#me" + #:client-id "https://app.client-authorization.scm" + #:redirect-uri "https://app.client-authorization.scm/redirected" + #:state "integrity&check" + #:http-get http-get)) + (expected-1 + `(("https://case-1.client-authorization.scm" + . ,(string->uri "https://case-1.client-authorization.scm/authorize?client_id=https%3A%2F%2Fapp.client-authorization.scm&redirect_uri=https%3A%2F%2Fapp.client-authorization.scm%2Fredirected&state=integrity%26check")))) + (expected-2 + `(("https://case-2.client-authorization.scm" + . ,(string->uri "https://case-2.client-authorization.scm/authorize?client_id=https%3A%2F%2Fapp.client-authorization.scm&redirect_uri=https%3A%2F%2Fapp.client-authorization.scm%2Fredirected&state=integrity%26check")) + ("https://one.identity.provider" + . ,(string->uri "https://one.identity.provider/authorize?client_id=https%3A%2F%2Fapp.client-authorization.scm&redirect_uri=https%3A%2F%2Fapp.client-authorization.scm%2Fredirected&state=integrity%26check")) + ("https://another.identity.provider" + . ,(string->uri "https://another.identity.provider/authorize?client_id=https%3A%2F%2Fapp.client-authorization.scm&redirect_uri=https%3A%2F%2Fapp.client-authorization.scm%2Fredirected&state=integrity%26check")))) + (expected-3 + `(("https://case-3.client-authorization.scm" + . ,(string->uri "https://case-3.client-authorization.scm/authorize?client_id=https%3A%2F%2Fapp.client-authorization.scm&redirect_uri=https%3A%2F%2Fapp.client-authorization.scm%2Fredirected&state=integrity%26check"))))) + (unless (equal? case-1 expected-1) + (format (current-error-port) "Case 1 failed:\n~s\n~s\n\n" + case-1 expected-1) + (exit 2)) + (unless (equal? (hash-map->list cons (alist->hash-table case-2)) + (hash-map->list cons (alist->hash-table expected-2))) + (format (current-error-port) "Case 2 failed:\n~s\n~s\n\n" + case-2 expected-2) + (exit 3)) + (unless (equal? case-3 expected-3) + (format (current-error-port) "Case 3 failed:\n~s\n~s\n\n" + case-3 expected-3) + (exit 4))))) diff --git a/tests/client-token.scm b/tests/client-token.scm new file mode 100644 index 0000000..02f5ec7 --- /dev/null +++ b/tests/client-token.scm @@ -0,0 +1,121 @@ +(use-modules (webid-oidc client) + (webid-oidc testing) + (webid-oidc token-endpoint) + (webid-oidc jwk) + (webid-oidc jti) + (webid-oidc authorization-code) + (webid-oidc oidc-configuration) + (webid-oidc jws) + (webid-oidc oidc-id-token) + (web uri) + (web request) + (web response) + (srfi srfi-19) + (ice-9 optargs) + (ice-9 receive) + (ice-9 hash-table)) + +(with-test-environment + "client-token" + (lambda () + (define the-current-time 0) + (define issuer-key (generate-key #:n-size 2048)) + (define issuer-configuration + (make-oidc-configuration + "https://issuer.client-token.scm/keys" + "https://issuer.client-token.scm/authorize" + "https://issuer.client-token.scm/token")) + (define token-endpoint (make-token-endpoint + (string->uri "https://issuer.client-token.scm/token") + (string->uri "https://issuer.client-token.scm") + 'RS256 + issuer-key + 3600 ;; 1 hour + (make-jti-list) + #:current-time (lambda () the-current-time))) + (define client-key (generate-key #:n-size 2048)) + (define authorization-code + (issue-authorization-code 'RS256 issuer-key 120 + (string->uri "https://client-token.scm/profile/card#me") + (string->uri "https://app.client-token.scm/app#id"))) + (define* (http-get uri #:key (headers '())) + (cond + ((equal? uri (string->uri "https://issuer.client-token.scm/.well-known/openid-configuration")) + (serve-oidc-configuration + (time-utc->date (make-time time-utc 0 (+ the-current-time 3600))) + issuer-configuration)) + ((equal? uri (string->uri "https://issuer.client-token.scm/keys")) + (serve-jwks + (time-utc->date (make-time time-utc 0 (+ the-current-time 3600))) + (make-jwks (list issuer-key)))) + (else + (format (current-error-port) "GET request to ~a: error.\n" (uri->string uri)) + (exit 1)))) + (define* (http-post uri #:key (body #f) (headers '())) + (unless (equal? uri (oidc-configuration-token-endpoint issuer-configuration)) + (format (current-error-port) + "Wrong URI for token negociation: ~a (expected ~a).\n" + (uri->string uri) + (uri->string + (oidc-configuration-token-endpoint + issuer-configuration))) + (exit 2)) + (unless (equal? body (format #f "grant_type=authorization_code&code=~a" + authorization-code)) + (format (current-error-port) + "Wrong body: ~s\n" body) + (exit 3)) + (unless (equal? + (assoc-ref headers 'content-type) + '(application/x-www-form-urlencoded)) + (format (current-error-port) + "Wrong content type: ~s\n" (assoc-ref headers 'content-type)) + (exit 4)) + (let ((request + (build-request uri + #:method 'POST + #:headers headers + #:port (open-input-string body))) + (request-body body)) + (token-endpoint request request-body))) + (let ((response + (token "https://issuer.client-token.scm" + client-key + #:authorization-code authorization-code + #:http-get http-get + #:http-post http-post + #:current-time (lambda () the-current-time)))) + (let ((id-token (assq-ref response 'id_token)) + (access-token (assq-ref response 'access_token)) + (token-type (assq-ref response 'token_type)) + (token-expiration (assq-ref response 'expires_in)) + (refresh-token (assq-ref response 'refresh_token))) + (let ((id-token-dec (id-token-decode id-token #:http-get http-get)) + (access-token-dec (jws-decode access-token (lambda (jws) issuer-key)))) + (unless id-token-dec + (format (current-error-port) "Could not decode the ID token from ~s (~s)" + id-token response) + (exit 5)) + (unless access-token-dec + (format (current-error-port) "Could not decode the access token from ~s (~s)" + access-token response) + (exit 6)) + (unless refresh-token + (format (current-error-port) "There does not seem to be a refresh token in ~s" + response) + (exit 6)) + (unless (equal? (id-token-webid id-token-dec) + (string->uri "https://client-token.scm/profile/card#me")) + (exit 7)) + (unless (equal? (id-token-iss id-token-dec) + (string->uri "https://issuer.client-token.scm")) + (exit 8)) + (unless (equal? (id-token-aud id-token-dec) + (string->uri "https://app.client-token.scm/app#id")) + (exit 9)) + ;; It’s not the job of the client to check that the access + ;; token is correct; TODO: add a check with a resource + ;; server. + + ;; TODO: try to negociate a refresh token. + ))))) -- cgit v1.2.3