;; disfluid, implementation of the Solid specification
;; Copyright (C) 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 (tests client-workflow)
#:use-module ((webid-oidc client) #:prefix client:)
#:use-module ((webid-oidc client accounts) #:prefix client:)
#:use-module ((webid-oidc jwk) #:prefix jwk:)
#:use-module (webid-oidc testing)
#:use-module (webid-oidc oidc-configuration)
#:use-module (webid-oidc server endpoint)
#:use-module (webid-oidc server endpoint resource-server)
#:use-module (webid-oidc server endpoint identity-provider)
#:use-module (webid-oidc server endpoint client)
#:use-module (webid-oidc server endpoint authentication)
#:use-module ((webid-oidc stubs) #:prefix stubs:)
#:use-module ((webid-oidc refresh-token) #:prefix refresh:)
#:use-module (webid-oidc simulation)
#:use-module ((webid-oidc parameters) #:prefix p:)
#:use-module (web uri)
#:use-module (web request)
#:use-module (web response)
#:use-module (srfi srfi-19)
#:use-module (srfi srfi-26)
#:use-module (ice-9 optargs)
#:use-module (ice-9 receive)
#:use-module (ice-9 hash-table)
#:use-module (ice-9 match)
#:use-module (oop goops)
#:declarative? #t
#:duplicates (merge-generics))
;; In this example, a user firsts requests an account, then logs in
;; with a refresh token, then logs out, but we can still revive per
;; account, then the refresh token gets banned.
(define client:)
(define client:)
(define (display-log simulation)
(format (current-error-port) "Log:\n")
(for-each
(match-lambda
((request request-body response response-body)
(format (current-error-port) "~s ~s (~s): ~s ~s\n"
(request-method request)
(uri->string (request-uri request))
request-body
(response-code response)
(response-reason-phrase response))))
(sim:simulation-scroll-log! simulation))
(exit 42))
(with-test-environment
"client-workflow"
(lambda ()
(let ((simulation
(make
#:endpoint
(make
#:routed
(list
(make
#:host "server.client-workflow.scm"
#:oidc-discovery
(make
#:path "/.well-known/openid-configuration"
#:configuration
(make
#:jwks-uri "https://server.client-workflow.scm/keys"
#:authorization-endpoint "https://server.client-workflow.scm/authorize"
#:token-endpoint "https://server.client-workflow.scm/token"))
#:authorization-endpoint
(make
#:path "/authorize"
#:subject "https://server.client-workflow.scm/alice#me"
#:encrypted-password (crypt "password" "$6$password")
#:key-file "key-file.jwk")
#:token-endpoint
(make
#:path "/token"
#:issuer "https://server.client-workflow.scm"
#:key-file "key-file.jwk")
#:jwks-endpoint
(make
#:path "/keys"
#:key-file "key-file.jwk")
#:default
(make
#:backend
(make
#:server-name "https://server.client-workflow.scm"
#:owner "https://server.client-workflow.scm/alice#me")
#:server-uri "https://server.client-workflow.scm"))
(make
#:host "client.client-workflow.scm"
#:client-id "https://client.client-workflow.scm/id"
#:redirect-uris '("https://client.client-workflow.scm/authorized")
#:client-name "Client workflow test"
#:client-uri "https://client.client-workflow.scm/about"
#:grant-types '(authorization_code refresh_token)
#:response-types '(code))))))
(account #f))
(parameterize ((client:client
(make
#:client-id "https://client.client-workflow.scm/id"
#:redirect-uri
(string->uri "https://client.client-workflow.scm/authorized")))
(p:anonymous-http-request
(cute (@ (webid-oidc simulation) request) simulation <...>)))
(parameterize ((p:current-date 0)
(client:authorization-process
(lambda* (uri #:key reason)
(grant-authorization simulation uri))))
(receive (new-account response response-body)
(begin
(set! account
(make #:issuer "https://server.client-workflow.scm"))
(client:request account
(string->uri "https://server.client-workflow.scm/")))
(set! account new-account)
(unless (eqv? (response-code response) 200)
;; Only Alice can read that resource.
(exit 3)))
(match (scroll-log! simulation)
;; 1. The client gets the oidc configuration of the
;; server.
;; 2. The browser gets redirected to the authorization
;; URI and POSTs the authorization form. The server makes
;; a request to the client ID, which replies first.
;; 3. The authorization request completes.
;; 4. The client exchanges the authorization code for a
;; refresh token.
;; 5. and 6. The client decodes the ID token and requests
;; the server keys.
;; 7. and 8. While the client is waiting for the final response to
;; complete, the server checks the access token validity by
;; querying the identity provider for its key.
;; 9. The client sends the signed request to the / URI of
;; the server.
(((get-oidc-config-request _ get-oidc-config-response _)
(get-client-id-request _ get-client-id-response _)
(authorization-request _ authorization-response _)
(token-request _ token-response _)
_ _ ;; the client gets the key
_ _ ;; the resource server gets the key
(final-request _ final-response _))
(unless
(and
;; 1. Get the authorization endpoint.
(equal? (request-uri get-oidc-config-request)
(string->uri "https://server.client-workflow.scm/.well-known/openid-configuration"))
(eqv? (response-code get-oidc-config-response) 200)
;; 2. The server checks the client ID.
(equal? (request-uri get-client-id-request)
(string->uri "https://client.client-workflow.scm/id"))
(eqv? (response-code get-client-id-response) 200)
;; 3. The authorization request completes.
(string-prefix?
"https://server.client-workflow.scm/authorize?"
(uri->string (request-uri authorization-request)))
(eq? (request-method authorization-request) 'POST)
(eqv? (response-code authorization-response) 302)
(string-prefix?
"https://client.client-workflow.scm/authorized?"
(uri->string (response-location authorization-response)))
;; 4. Token negociation.
(equal? (request-uri token-request)
(string->uri "https://server.client-workflow.scm/token"))
(eqv? (response-code token-response) 200)
;; 5. The final request.
(equal? (request-uri final-request)
(string->uri "https://server.client-workflow.scm/"))
(eqv? (response-code final-response) 200))
(exit 4)))))
;; 1 hour later, the access token should have expired.
(parameterize ((p:current-date 3600))
(receive (new-account response response-body)
(client:request account (string->uri "https://server.client-workflow.scm/"))
(set! account new-account)
(unless (eqv? (response-code response) 200)
;; Only Alice can read that resource.
(exit 5)))
(match (scroll-log! simulation)
;; 1. and 2. The client starts sending the request, the server
;; querries the identity provider and keys.
;; 3. The client directly sends the request. It fails because
;; the access token expired.
;; 4. The client queries the OIDC configuration to get the
;; token endpoint.
;; 5. The client gets an access token from the refresh token.
;; 6. 7. The client decodes the ID token, by getting the keys
;; again.
;; 8. and 9. The client starts sending the new request, the
;; server checks the access token.
;; 10. The client sends the request again, and it succeeds.
((_
_
(naively-try-request _ naively-try-response _)
(get-token-endpoint-request _ get-token-endpoint-response _)
(refresh-request _ refresh-response _)
_ _ _ _
(with-new-refresh-token-request _ with-new-refresh-token-response _))
(unless
(and
;; 3. The client realizes that the access token is
;; expired.
(equal? (request-uri naively-try-request)
(string->uri "https://server.client-workflow.scm/"))
(eqv? (response-code naively-try-response) 401)
(eqv? (time-second (date->time-utc (response-date naively-try-response)))
3600)
;; 4. The client discovers the token endpoint.
(equal? (request-uri get-token-endpoint-request)
(string->uri "https://server.client-workflow.scm/.well-known/openid-configuration"))
(eqv? (response-code get-token-endpoint-response) 200)
;; 5. Refresh the access token.
(equal? (request-uri refresh-request)
(string->uri "https://server.client-workflow.scm/token"))
(eqv? (response-code refresh-response) 200)
;; 10. Send again.
(equal? (request-uri with-new-refresh-token-request)
(string->uri "https://server.client-workflow.scm/"))
(eqv? (response-code with-new-refresh-token-response) 200))
(exit 6)))))
;; Wait another hour, and we’ll need to update the refresh
;; token again, but this time it’s not there anymore.
(parameterize ((p:current-date 7200))
(refresh:remove-refresh-token
(string->uri "https://server.client-workflow.scm/alice#me")
(string->uri "https://client.client-workflow.scm/id"))
(with-exception-handler
(lambda (error)
(unless (client:refresh-token-expired? error)
(exit 7)))
(lambda ()
(client:request account (string->uri "https://server.client-workflow.scm/"))
(exit 8))
#:unwind? #t
#:unwind-for-type client:&refresh-token-expired)
(match (scroll-log! simulation)
;; 1. and 2. The client starts sending the request, the server
;; querries the identity provider and keys.
;; 3. The client directly sends the request. It fails
;; because the access token expired.
;; 4. The client queries the OIDC configuration to get the
;; token endpoint.
;; 5. The client sends the token request, but it fails with
;; 403.
((_
_
(naively-try-request _ naively-try-response _)
(get-token-endpoint-request _ get-token-endpoint-response _)
(refresh-request _ refresh-response _))
;; 3. The client realizes that the access token is
;; expired.
(equal? (request-uri naively-try-request)
(string->uri "https://server.client-workflow.scm/"))
(eqv? (response-code naively-try-response) 401)
(eqv? (time-second (date->time-utc (response-date naively-try-response)))
7200)
;; 4. The client discovers the token endpoint.
(equal? (request-uri get-token-endpoint-request)
(string->uri "https://server.client-workflow.scm/.well-known/openid-configuration"))
(eqv? (response-code get-token-endpoint-response) 200)
;; 5. The client tries to refresh.
(equal? (request-uri refresh-request)
(string->uri "https://server.client-workflow.scm/token"))
(eqv? (response-code refresh-response) 403))))))))