git.fiddlerwoaroof.com
cl-oid-connect.lisp
373a5a27
 ;;;; cl-oid-connect.lisp
 #|
  |Copyright (c) 2015 Edward Langley
  |All rights reserved.
  |
  |Redistribution and use in source and binary forms, with or without
  |modification, are permitted provided that the following conditions
  |are met:
  |
  |Redistributions of source code must retain the above copyright notice,
  |this list of conditions and the following disclaimer.
  |
  |Redistributions in binary form must reproduce the above copyright
  |notice, this list of conditions and the following disclaimer in the
  |documentation and/or other materials provided with the distribution.
  |
  |Neither the name of the project's author nor the names of its
  |contributors may be used to endorse or promote products derived from
  |this software without specific prior written permission.
  |
  |THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  |"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  |LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  |FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  |HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  |SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES  INCLUDING, BUT NOT LIMITED
  |TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  |PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  |LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  |NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  |SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  |
  |#
 
 (in-package :cl-oid-connect)
 (setq drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
 
 (with-open-file (goog-info #P"google-secrets.json")
   (let* ((data (yason:parse goog-info))
          (client-id (gethash "client-id" data))
          (secret (gethash "secret" data)))
     (defconstant *GOOG-CLIENT-ID* client-id)
     (defconstant *GOOG-CLIENT-SECRET* secret)))
 
 ;;; "cl-oid-connect" goes here. Hacks and glory await!
 (defvar *app* (make-instance 'ningle:<app>))
 (defvar *state* nil)
 
 ;; These tokens specify the auth endpoint. These are autodiscovered, if the relevant
 ;; functions are wrapped with the "with-goog-endpoints" macro.
 (defvar *auth-endpoint* nil)
 (defvar *token-endpoint* nil)
 
 (defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal))
 (defmacro assoc-cdr (key alist) `(cdr (assoc ,key ,alist)))
 
 (defmacro with-goog-endpoints (&body body)
   (alexandria:with-gensyms (discovery-document)
     `(let* ((,discovery-document
               (cl-json:decode-json-from-string
                 (drakma:http-request "https://accounts.google.com/.well-known/openid-configuration")))
             (*auth-endpoint* (assoc-cdr :authorization--endpoint ,discovery-document))
             (*token-endpoint* (assoc-cdr :token--endpoint ,discovery-document)))
        ,@body)))     
 
 (defvar *goog-mw*
   (lambda (app)
     (lambda (env)
       (with-goog-endpoints
         (funcall app env)))))
 
 (defun get-access-token (code)
   (cl-json:decode-json-from-string
     (drakma:http-request *token-endpoint*
                          :method :post
                          :redirect nil
                          :parameters `(("code" . ,code)
                                        ("client_id" . ,*GOOG-CLIENT-ID*)
                                        ("client_secret" . ,*GOOG-CLIENT-SECRET*)
                                        ("redirect_uri" . "http://srv2.elangley.org:9090/oidc_callback")
                                        ("grant_type" . "authorization_code")))))
 
 (defun do-auth-request (state)
   (drakma:http-request *auth-endpoint*
                        :redirect nil
                        :parameters `(("client_id" . ,*GOOG-CLIENT-ID*)
                                      ("response_type" . "code")
                                      ("scope" . "openid email")
                                      ("redirect_uri" . "http://srv2.elangley.org:9090/oidc_callback")
                                      ("state" . ,state))))
 
 (defun gen-state (len)
   (with-output-to-string (stream)
     (let ((*print-base* 36))
       (loop repeat len
             do (princ (random 36) stream)))))
 
 (defmacro def-route (url args &body body)
   `(setf (ningle:route *app* ,url)
          #'(lambda ,args
              ,@body)))
 
 (defmacro check-state (received-state then else)
   (alexandria:with-gensyms (saved-state)
     `(let ((,saved-state (gethash :state *session*)))
        (if (equal ,saved-state ,received-state)
          ,then
          ,else))))
 
 (defmacro require-login (&body body)
   `(if (not (eql nil (gethash :userinfo *session*)))
      (progn
        ,@body)
      '(302 (:location "/login"))))
 
 (def-route "/login" (params)
   (declare (ignore params))
   (let ((state (gen-state 36)))
     (setf (gethash :state *session*) state)
     (multiple-value-bind (content rcode headers) (do-auth-request state)
       (if (< rcode 400)
         `(302 (:location ,(cdr (assoc :location headers))))
         content))))
 
 (def-route "/oidc_callback" (params)
   (let ((received-state (cdr (string-assoc "state" params)))
         (code (cdr (string-assoc "code" params))))
     (check-state received-state
                  (let* ((a-t (get-access-token code)) (id-token (assoc-cdr :id--token a-t))
                         (decoded (cljwt:decode id-token :fail-if-unsupported nil)))
                    (setf (gethash :userinfo *session*) decoded)
                    '(302 (:location "/")))
                  '(403 '() "Out, vile imposter!"))))
 
 (def-route "/" (params)
   (require-login 
     (anaphora:sunless (gethash :counter *session*) (setf anaphora:it 0))
     (format nil "~Ath visit<br/>~a<br/>~S"
             (incf (gethash :counter *session*))
             *state*
             (alexandria:hash-table-alist *session*))))
 
 
 
 (setf *handler* (clack:clackup (lack.builder:builder :session *goog-mw* *app*)
                                :port 9090))