;; disfluid, implementation of the Solid specification
;; Copyright (C) 2020, 2021 Vivien Kraus
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see .
(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 parameters) #:prefix p:)
#:use-module ((webid-oidc stubs) #:prefix stubs:)
#:use-module ((webid-oidc config) #:prefix cfg:)
#:use-module ((webid-oidc cache) #:prefix cache:)
#:use-module ((webid-oidc client accounts) #:prefix account:)
#:use-module ((webid-oidc client client) #:prefix client:)
#:use-module (web uri)
#:use-module (web client)
#:use-module (web request)
#: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-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 (ice-9 match)
#:use-module (sxml simple)
#:use-module (oop goops)
#:re-export
(
(client: . )
(client:client-id . client-id)
(client:client-key-pair . client-key-pair)
(client:client-redirect-uri . client-redirect-uri)
(client:client . client)
(account:authorization-process . authorization-process)
(account:authorization-state . authorization-state)
(account:anonymous-http-request . anonymous-http-request)
(client:->sexp . ->sexp)
)
#:export
(
request
serve-application
)
#:declarative? #t)
;; For syntax highlighting
(define account:)
(define client:)
(define (setup-headers!)
;; 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"
(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))))))
(define default-http-get-with-cache
(cache:with-cache))
(define* (default-http-request uri . all-args)
(let try-get-with-cache ((args all-args))
(match args
((#:headers _)
(apply default-http-get-with-cache all-args))
((#:headers _ other-args ...)
(try-get-with-cache other-args))
(else
(apply http-request all-args)))))
(define* (initial-login client issuer
#:key
(http-request default-http-request))
(setup-headers!)
(parameterize ((account:anonymous-http-request default-http-request)
(client:client client))
(make
#:issuer issuer)))
(define (request account uri . other-args)
(setup-headers!)
(unless (account:access-token account)
(set! account (account:refresh account)))
(define (do-with-headers method headers non-header-args can-fail?)
(let* ((access-token (account:access-token account))
(dpop-proof
(let ((key-pair (account:key-pair account)))
(issue-dpop-proof
key-pair
#:htm method
#:htu uri
#:access-token access-token))))
(let ((all-headers
`((dpop . ,dpop-proof)
(authorization . (dpop . ,access-token))
,@headers)))
(receive (response body)
(apply (account:anonymous-http-request) uri
#:headers all-headers
non-header-args)
(let ((code (response-code response)))
(if (and (eqv? code 401) can-fail?)
;; Code expired
(begin
(set! account (account:refresh (account:invalidate-access-token account)))
;; retry
(do-with-headers method headers non-header-args #f))
(values account response body)))))))
(let scan-arguments ((args other-args)
(headers #f)
(non-header-args '())
(method #f))
(match args
(()
(cond
((not headers)
(scan-arguments args '() non-header-args method))
((not method)
(scan-arguments args headers non-header-args 'GET))
(else
(do-with-headers method headers (reverse non-header-args) #t))))
((#:method new-method args ...)
(scan-arguments args headers non-header-args (or method new-method)))
((#:headers (new-headers ...) args ...)
(scan-arguments args (or headers new-headers) non-header-args method))
((kw value args ...)
(scan-arguments args headers `(,value ,kw ,@non-header-args) method)))))
(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)
(set! redirect-uri (string->uri redirect-uri)))
(when (string? client-uri)
(set! client-uri (string->uri client-uri)))
(let* ((manifest
(format #f
"{
\"@context\": \"https://www.w3.org/ns/solid/oidc-context.jsonld\",
\"client_id\" : \"~a\",
\"redirect_uris\" : [\"~a\"],
\"client_name\" : \"~a\",
\"client_uri\" : \"~a\",
\"grant_types\" : [\"refresh_token\", \"authorization_code\"],
\"response_types\" : [\"code\"]
}
"
(uri->string id)
(uri->string redirect-uri)
client-name
(uri->string id)))
(manifest-etag (stubs:hash 'SHA-256 manifest)))
(lambda (request request-body)
(let ((uri (request-uri request)))
(cond
((equal? (uri-path uri) (uri-path id))
(let ((if-none-match (request-if-none-match request)))
(if (and (list? if-none-match)
(member manifest-etag
(map car (request-if-none-match request))))
(values
(build-response
#:code 304
#:reason-phrase "Not Modified"
#:headers `((content-type application/ld+json)
(etag . (,manifest-etag . #t))))
#f)
(values
(build-response
#:headers `((content-type application/ld+json)
(etag . (,manifest-etag . #t))
(cache-control public must-revalidate)))
manifest))))
((equal? (uri-path uri) (uri-path redirect-uri))
(let ((query-args
(map
(lambda (key=value)
(let ((splits
(map uri-decode (string-split key=value #\=))))
(if (or (null? splits) (null? (cdr splits)))
splits
(cons (string->symbol (car splits)) (cdr splits)))))
(string-split (uri-query uri) #\&))))
(let ((code (assq-ref query-args 'code)))
(if code
(values
(build-response
#:headers `((content-type application/xhtml+xml)))
(with-output-to-string
(lambda ()
(sxml->xml
`(*TOP*
(*PI* xml "version=\"1.0\" encoding=\"utf-8\"")
(html (@ (xmlns "http://www.w3.org/1999/xhtml")
(xml:lang "en"))
(head
(title "Authorization"))
(body
(p "You have been authorized. Please paste the following code in the application:")
(p (strong ,code)))))))))
(values
(build-response
#:code 400
#:reason-phrase "Invalid Request"
#:headers `((content-type application/xhtml+xml)))
(with-output-to-string
(lambda ()
(sxml->xml
`(*TOP*
(*PI* xml "version=\"1.0\" encoding=\"utf-8\"")
(html (@ (xmlns "http://www.w3.org/1999/xhtml")
(xml:lang "en"))
(head
(title "Error"))
(body
(p "Your identity provider did not authorize you. :("))))))))))))
(else
(values
(build-response
#:code 404
#:reason-phrase "Not Found"
#:headers `((content-type application/xhtml+xml)))
(with-output-to-string
(lambda ()
(sxml->xml
`(*TOP*
(*PI* xml "version=\"1.0\" encoding=\"utf-8\"")
(html (@ (xmlns "http://www.w3.org/1999/xhtml")
(xml:lang "en"))
(head
(title "Not Found"))
(body
(p "This page does not exist on the server."))))))))))))))