summaryrefslogtreecommitdiff
path: root/src/scm/webid-oidc/client.scm
diff options
context:
space:
mode:
Diffstat (limited to 'src/scm/webid-oidc/client.scm')
-rw-r--r--src/scm/webid-oidc/client.scm551
1 files changed, 136 insertions, 415 deletions
diff --git a/src/scm/webid-oidc/client.scm b/src/scm/webid-oidc/client.scm
index 67928db..4fdb824 100644
--- a/src/scm/webid-oidc/client.scm
+++ b/src/scm/webid-oidc/client.scm
@@ -24,6 +24,7 @@
#:use-module ((webid-oidc parameters) #:prefix p:)
#:use-module ((webid-oidc stubs) #:prefix stubs:)
#:use-module ((webid-oidc config) #:prefix cfg:)
+ #:use-module ((webid-oidc client accounts) #:prefix client:)
#:use-module (web uri)
#:use-module (web client)
#:use-module (web request)
@@ -32,434 +33,154 @@
#:use-module (web http)
#:use-module (ice-9 optargs)
#:use-module (ice-9 receive)
+ #:use-module (srfi srfi-9)
#:use-module (srfi srfi-19)
+ #:use-module (srfi srfi-26)
#:use-module (rnrs bytevectors)
#:use-module (ice-9 i18n)
#:use-module (ice-9 getopt-long)
#:use-module (ice-9 suspendable-ports)
- #:use-module (sxml simple))
+ #:use-module (ice-9 match)
+ #:use-module (sxml simple)
+ #:export
+ (
+ <client>
+ make-client
+ client?
+ client-id
+ client-key
+ client-redirect-uri
-(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*-public (token host client-key
- #:key
- (authorization-code #f)
- (refresh-token #f)
- (http-get http-get)
- (http-post http-post))
- (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)))
- (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)))
- (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-public (list-profiles)
- (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 (p:data-home) "/profiles")
- read))
- (lambda error
- (format (current-error-port) "Could not read profiles: ~s\n" error)
- '()))))
-
-(define (add-profile webid issuer refresh-token key)
- (let ((other-profiles (list-profiles)))
- (stubs:atomically-update-file
- (string-append (p:data-home) "/profiles")
- (string-append (p:data-home) "/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)
- (http-get http-get)
- (http-post http-post))
- (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)))
- (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))
- (values (cdr id-token) access-token key)))))))))
-
-(define*-public (login webid issuer refresh-token key
- #:key
- (http-get http-get)
- (http-post http-post))
- (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)))
- (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))
- (values (cdr id-token) access-token key)))))
+ request
-(define*-public (refresh id-token
- key
- #:key
- (http-get http-get)
- (http-post http-post))
- (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)))
- (letrec ((find-refresh-token
- (lambda (profiles)
- (when (null? profiles)
- (raise-profile-not-found (id-token-webid id-token)
- (id-token-iss id-token)
- (p:data-home)))
- (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
- #:http-get http-get
- #:http-post http-post))))
+ serve-application
+ )
+ #:declarative? #t)
-(define* (renew-if-expired id-token access-token key
- date
- #:key
- (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)
- (parameterize ((p:current-date (lambda () date)))
- (refresh id-token key
- #:http-get http-get
- #:http-post http-post))
- (values id-token access-token key))))
+(define-record-type <client>
+ (make-client id key redirect-uri)
+ client?
+ (id client-id)
+ (key client-key)
+ (redirect-uri client-redirect-uri))
-(define*-public (make-client id-token access-token key
- #:key
- (http-get http-get)
- (http-post http-post)
- (http-request http-request))
+;; subject is optional, if you don’t know who the user is.
+(define* (request client subject issuer
+ #:key
+ (http-request http-request))
;; 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 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
- #:access-token access-token)))
- (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
- #: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 #f))))
- (values response response-body))))))
- (define (parse-args uri method headers other-args-rev rest)
- (if (null? rest)
- (handler uri method headers (reverse other-args-rev) #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)))
+ (declare-header!
+ "WWW-Authenticate"
+ (cute parse-header 'pragma <>)
+ (lambda (value)
+ (and (list? value)
+ (let check-value ((schemes value))
+ (match schemes
+ (() #t)
+ (((hd . args) tl ...)
+ (and (symbol? hd)
+ (let check-args ((args args))
+ (match args
+ (() #t)
+ (((key . value) tl ...)
+ (and (symbol? key)
+ (string? value)
+ (check-args tl)))))
+ (check-value tl)))))))
+ (cute write-header 'pragma <> <>))
+ ;; The same applies for the authorization header.
+ (let ((original-parser (header-parser 'authorization))
+ (original-writer (header-writer 'authorization)))
+ (declare-header!
+ "Authorization"
+ original-parser
+ (lambda (value) #t)
+ (match-lambda*
+ ((('dpop . dpop) port)
+ (format port "DPoP ~a" dpop))
+ ((value port)
+ (original-writer value port)))))
+ (match client
+ (($ <client> client-id client-key redirect-uri)
+ (let ((do-login
+ (let ((my-http-get
+ (lambda* (uri . args)
+ (apply http-request uri
+ #:method 'GET
+ args)))
+ (my-http-post
+ (lambda* (uri . args)
+ (apply http-request uri
+ #:method 'POST
+ args))))
+ (match-lambda*
+ ((subject issuer)
+ (client:save-account
+ (client:login subject issuer
+ #:http-get my-http-get
+ #:http-post my-http-post
+ #:client-id client-id
+ #:client-key client-key
+ #:redirect-uri redirect-uri)))
+ (($ <account> subject issuer _ _ _ _)
+ (client:save-account
+ (client:login subject issuer
+ #:http-get my-http-get
+ #:http-post my-http-post
+ #:client-id client-id
+ #:client-key client-key
+ #:redirect-uri redirect-uri)))))))
+ (let ((current-account (do-login subject issuer)))
+ (define (handle request request-body)
+ (receive (response response-body)
+ (let* ((access-token (client:account-access-token current-account))
+ (dpop-proof
+ (issue-dpop-proof
+ (client:account-keypair current-account)
+ #:alg (case (kty client-key)
+ ((EC) 'ES256)
+ ((RSA) 'RS256))
+ #:htm (request-method request)
+ #:htu (request-uri request)
+ #:access-token access-token)))
+ (let ((headers
+ `((dpop . ,dpop-proof)
+ (authorization . (dpop . ,access-token))
+ ,@(request-headers request))))
+ (http-request
+ (request-uri request)
+ #:method (request-method request)
+ #:headers headers)))
+ (if (eqv? (response-code response) 401)
+ ;; Maybe the accesss token expired
+ (let ((server-date (time-second (date->time-utc (response-date response))))
+ (exp (assq-ref (client:account-id-token current-account) 'exp)))
+ (if (>= server-date exp)
+ ;; The ID token expired, renew it.
+ (begin
+ (set! current-account
+ (client:save-account
+ (do-login
+ (client:save-account
+ (client:invalidate-access-token current-account)))))
+ ;; Read it that way: invalidate the current
+ ;; account access token, then save it so that
+ ;; noone uses the invalid access token, then
+ ;; try to log in again, and finally save the
+ ;; new access token.
+ (handle request request-body))
+ ;; The ID token has not expired, we don’t care.
+ (values response response-body)))
+ ;; OK or other error, we don’t care.
+ (values response response-body))))
+ handle)))))
-(define*-public (serve-application id redirect-uri
- #:key
- (client-name "Example application")
- (client-uri "https://webid-oidc-demo.planete-kraus.eu"))
+(define* (serve-application id redirect-uri
+ #:key
+ (client-name "Example application")
+ (client-uri "https://webid-oidc-demo.planete-kraus.eu"))
(when (string? id)
(set! id (string->uri id)))
(when (string? redirect-uri)