;; 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-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 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:key-pair . key-pair) (client:redirect-uri . redirect-uri) (client:client . client) (account:authorization-process . authorization-process) (account:authorization-state . authorization-state) ) #: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* (initial-login client issuer) (setup-headers!) (parameterize ((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 key-pair #:jwk (public-key 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 (p: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."))))))))))))))