373a5a27 |
;;;; cl-oid-connect.lisp
#|
|
a43b3cd8 |
|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.
|
|#
|
373a5a27 |
(in-package :cl-oid-connect)
|
a43b3cd8 |
; Should this be here?
(setf drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
(sheeple:defproto =service-info= ()
((client-id nil :accessor t)
(secret nil :accessor t)))
(defvar *FBOOK-INFO* (sheeple:clone =service-info=))
(defun load-facebook-info (loadfrom)
(setf *FBOOK-INFO*
(with-open-file (fbook-info (truename loadfrom))
(let* ((data (yason:parse fbook-info))
(client-id (gethash "client-id" data))
(secret (gethash "secret" data)))
(sheeple:defobject (=service-info=)
((client-id client-id)
(secret secret)))))))
(defvar *GOOG-INFO* (sheeple:clone =service-info=))
(defun load-google-info (loadfrom)
(setf *GOOG-INFO*
(with-open-file (goog-info (truename loadfrom))
(let* ((data (yason:parse goog-info))
(client-id (gethash "client-id" data))
(secret (gethash "secret" data)))
(sheeple:defobject (=service-info=)
((client-id client-id)
(secret secret)))))))
(load-facebook-info #p"facebook-secrets.json")
(load-google-info #p"google-secrets.json")
|
373a5a27 |
(defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal))
(defmacro assoc-cdr (key alist) `(cdr (assoc ,key ,alist)))
|
a43b3cd8 |
(sheeple:defproto =endpoint-schema= ()
((auth-endpoint nil :accessor t)
(token-endpoint nil :accessor t)
(redirect-uri nil :accessor t)))
(sheeple:defmessage get-user-info (a b))
(sheeple:defmessage get-access-token (a b))
(sheeple:defreply get-user-info ((a =endpoint-schema=) (b sheeple:=string=)))
(sheeple:defreply get-access-token ((a =endpoint-schema=) (b sheeple:=string=)))
(defun discover-endpoints (service-info discovery-doc-url &key (gat nil gat-p) (gui nil gui-p))
(let ((discovery-document (cl-json:decode-json-from-string (drakma:http-request discovery-doc-url)))
(schema (sheeple:object :parents `(,=endpoint-schema= ,service-info))))
(setf (auth-endpoint schema) (assoc-cdr :authorization--endpoint discovery-document))
(setf (token-endpoint schema) (assoc-cdr :token--endpoint discovery-document))
(if gui-p (sheeple:defreply get-user-info ((a schema)) (funcall gui a)))
(if gat-p (sheeple:defreply get-access-token ((a schema) (b sheeple:=string=))
(funcall gat a b)))
schema))
; This probably should eventually go?
(defvar *endpoint-schema* nil)
(defmacro with-endpoints (endpoint-schema &body body)
`(let* ((*endpoint-schema* ,endpoint-schema))
,@body))
(defun goog-get-access-token (endpoint-schema code)
(cl-json:decode-json-from-string
(drakma:http-request (token-endpoint endpoint-schema)
:method :post
:redirect nil
:parameters `(("code" . ,code)
("client_id" . ,(client-id endpoint-schema))
("client_secret" . ,(secret endpoint-schema))
("redirect_uri" . ,(redirect-uri endpoint-schema))
("grant_type" . "authorization_code")))))
; goog is well behaved
(defvar *goog-endpoint-schema*
(discover-endpoints *GOOG-INFO* "https://accounts.google.com/.well-known/openid-configuration"
:gat #'goog-get-access-token))
(setf (redirect-uri *goog-endpoint-schema*) "http://srv2.elangley.org:9090/oidc_callback/google")
; fbook needs personal attention
(defproto *fbook-endpoint-schema* (=endpoint-schema= *fbook-info*)
((auth-endpoint "https://www.facebook.com/dialog/oauth")
(token-endpoint "https://graph.facebook.com/v2.3/oauth/access_token")
(userinfo-endpoint "https://graph.facebook.com/v2.3/me" :accessor t)
(redirect-uri "http://srv2.elangley.org:9090/oidc_callback/facebook")))
(sheeple:defreply get-access-token ((endpoint-schema *fbook-endpoint-schema*) (code sheeple:=string=))
(cl-json:decode-json-from-string
(drakma:http-request (token-endpoint endpoint-schema)
:method :post
:redirect nil
:parameters `(("code" . ,code)
("client_id" . ,(client-id endpoint-schema))
("app_id" . ,(client-id endpoint-schema))
("client_secret" . ,(secret endpoint-schema))
("redirect_uri" . ,(redirect-uri endpoint-schema))
("grant_type" . "authorization_code")
("")
))))
(sheeple:defreply get-user-info ((endpoint-schema *fbook-endpoint-schema*) (access-token sheeple:=string=))
(let ((endpoint (userinfo-endpoint endpoint-schema)))
|
3573d7f1 |
(cl-json:decode-json-from-string
(drakma:http-request endpoint
:parameters `(("access_token" . ,access-token))))))
(defvar *fbook-mw*
(lambda (app)
(lambda (env)
(with-fbook-endpoints
(format t "~a" *client-id*)
(funcall app env)))))
|
373a5a27 |
(defvar *goog-mw*
(lambda (app)
(lambda (env)
(with-goog-endpoints
(funcall app env)))))
|
a43b3cd8 |
(defun do-auth-request (endpoint-schema state)
(format t "~%client-id: ~a~%" (auth-endpoint endpoint-schema))
(drakma:http-request (auth-endpoint endpoint-schema)
|
373a5a27 |
:redirect nil
|
a43b3cd8 |
:parameters `(("client_id" . ,(client-id endpoint-schema))
("app_id" . ,(client-id endpoint-schema))
|
373a5a27 |
("response_type" . "code")
|
3573d7f1 |
("scope" . "email")
|
a43b3cd8 |
("redirect_uri" . ,(redirect-uri endpoint-schema))
|
373a5a27 |
("state" . ,state))))
(defun gen-state (len)
(with-output-to-string (stream)
(let ((*print-base* 36))
(loop repeat len
do (princ (random 36) stream)))))
|
a43b3cd8 |
(defvar *app* (make-instance 'ningle:<app>))
|
373a5a27 |
(defmacro def-route (url args &body body)
`(setf (ningle:route *app* ,url)
#'(lambda ,args
|
3573d7f1 |
(declare (ignorable ,@args))
|
373a5a27 |
,@body)))
(defmacro check-state (received-state then else)
|
3573d7f1 |
(alexandria:with-gensyms (saved-state session)
`(let* ((,session (context :session))
(,saved-state (gethash :state ,session)))
|
373a5a27 |
(if (equal ,saved-state ,received-state)
,then
,else))))
(defmacro require-login (&body body)
|
3573d7f1 |
(alexandria:with-gensyms (session)
`(let ((,session (context :session)))
(if (not (eql nil (gethash :userinfo ,session)))
(progn
,@body)
|
a43b3cd8 |
(progn
(setf (gethash :next-page session) (lack.request:request-path-info *request*))
'(302 (:location "/login")))))))
|
3573d7f1 |
(defmacro with-session ((var) &body body)
`(let ((,var (context :session)))
,@body))
|
373a5a27 |
|
3573d7f1 |
(def-route "/login/google" (params)
(with-session (session)
(let ((state (gen-state 36)))
(setf (gethash :state session) state)
|
a43b3cd8 |
(with-endpoints *goog-endpoint-schema*
(setf (gethash :endpoint-schema session) *goog-endpoint-schema*)
(multiple-value-bind (content rcode headers) (do-auth-request *goog-endpoint-schema* state)
|
3573d7f1 |
(if (< rcode 400)
`(302 (:location ,(cdr (assoc :location headers))))
content))))))
(def-route "/login/facebook" (params)
(with-session (session)
(let ((state (gen-state 36)))
(setf (gethash :state session) state)
|
a43b3cd8 |
(with-endpoints *fbook-endpoint-schema*
(setf (gethash :endpoint-schema session) *fbook-endpoint-schema*)
(multiple-value-bind (content rcode headers uri) (do-auth-request *fbook-endpoint-schema* state)
|
3573d7f1 |
(if (< rcode 400)
`(302 (:location ,(format nil "~a" uri)))
content))))))
(def-route "/oidc_callback/google" (params)
|
373a5a27 |
(let ((received-state (cdr (string-assoc "state" params)))
(code (cdr (string-assoc "code" params))))
(check-state received-state
|
3573d7f1 |
(with-session (session)
|
a43b3cd8 |
(with-endpoints *goog-endpoint-schema*
(let* ((a-t (get-access-token *goog-endpoint-schema* code))
(id-token (assoc-cdr :id--token a-t))
|
3573d7f1 |
(decoded (cljwt:decode id-token :fail-if-unsupported nil)))
(setf (gethash :userinfo session) decoded)
'(302 (:location "/")))))
|
373a5a27 |
'(403 '() "Out, vile imposter!"))))
|
3573d7f1 |
(def-route "/oidc_callback/facebook" (params)
(let ((received-state (cdr (string-assoc "state" params)))
(code (cdr (string-assoc "code" params))))
|
a43b3cd8 |
(with-endpoints *fbook-endpoint-schema*
|
3573d7f1 |
(check-state received-state
(with-session (session)
|
a43b3cd8 |
(let* ((a-t (get-access-token *fbook-endpoint-schema* code))
|
3573d7f1 |
(id-token (assoc-cdr :access--token a-t)))
|
a43b3cd8 |
(setf (gethash :userinfo session) (get-user-info *fbook-endpoint-schema* id-token))
|
3573d7f1 |
'(302 (:location "/"))))
'(403 '() "Out, vile imposter!")))))
(def-route "/userinfo.json" (params)
(with-session (session)
(require-login
|
a43b3cd8 |
(with-endpoints (gethash :endpoint-schema session)
|
3573d7f1 |
(cl-json:encode-json-to-string (gethash :userinfo session))))))
(def-route "/logout" (params)
(with-session (session)
(setf (gethash :userinfo session) nil)
'(302 (:location "/"))))
|
a43b3cd8 |
(def-route "/login" (params)
(cl-who:with-html-output-to-string (s)
(:html
(:head
(:title "Login"))
(:body
(:div (:a :href "/login/facebook" "Facebook"))
(:div (:a :href "/login/google" "Google"))))))
|
373a5a27 |
(def-route "/" (params)
|
3573d7f1 |
(with-session (session)
|
a43b3cd8 |
(if (not (null (gethash :next-page session)))
`(302 (:location ,(gethash :next-page session)))
(require-login
(anaphora:sunless (gethash :counter session) (setf anaphora:it 0))
(format nil "~Ath visit<br/>~a<br/><br/>~S<br/>"
(gethash :counter session)
(alexandria:hash-table-alist session)
(alexandria:hash-table-alist (context :session)))))))
|
373a5a27 |
|
3573d7f1 |
(setf *handler* (clack:clackup (lack.builder:builder :session *app*) :port 9090))
|
373a5a27 |
|