git.fiddlerwoaroof.com
Browse code

Splitting up and prettifying cl-oid-connect

fiddlerwoaroof authored on 04/11/2015 23:46:27
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,5 @@
1
+*-secrets.json
2
+.*.sw?
3
+*.fasl
4
+bin/*
5
+[#]*[#]
... ...
@@ -18,10 +18,12 @@
18 18
                 :plump
19 19
                 :cl-who
20 20
                 :postmodern
21
-                :iterate)
21
+                :iterate
22
+                :fwoar.lisputils)
22 23
   :serial t
23
-  :components ((:file "utils")
24
-               (:file "package")
24
+  :components ((:file "package")
25
+               (:file "utils")  
26
+               (:file "objects")  
25 27
                (:file "cl-oid-connect")))
26 28
 
27 29
 
... ...
@@ -37,215 +37,23 @@
37 37
 (in-package :cl-oid-connect)
38 38
 ; Should this be here?
39 39
 
40
-(eval-when (:compile-toplevel :execute :load-toplevel)
41
-  (defun vars-to-symbol-macrolets (vars obj)
42
-    (iterate:iterate (iterate:for (store key) in (ensure-mapping vars))
43
-                     (iterate:collect `(,store (gethash ,(alexandria:make-keyword key) ,obj))))))
44
-
45
-(defmacro with-session-values (vars session &body body)
46
-  (alexandria:once-only (session)
47
-    `(symbol-macrolet ,(vars-to-symbol-macrolets vars session)
48
-       ,@body)))
49
-
50
-; This probably should eventually go?
51
-(defmacro with-endpoints (endpoint-schema  &body body)
52
-  `(let* ((*endpoint-schema* ,endpoint-schema))
53
-     ,@body))
54
-
55
-(defmacro with-session ((var) &body body)
56
-  `(progn
57
-     (format t "The session var is: ~a it contains: ~a~%"  ,(symbol-name var) ,var)
58
-     (let ((,var (context :session)))
59
-       (format t "The session var is: ~a it now contains: ~a~%"  ,(symbol-name var) ,var)
60
-       ,@body)))
61
-
62
-(defmacro def-route ((url args &key (app *oid*) (method :GET)) &body body)
63
-  `(setf (ningle:route ,app ,url :method ,method)
64
-         (lambda ,args
65
-           (declare (ignorable ,@args))
66
-           ,@body)))
67
-
68
-(defun gen-state (len)
69
-  (with-output-to-string (stream)
70
-    (let ((*print-base* 36))
71
-      (loop repeat len
72
-            do (princ (random 36) stream)))))
73
-
74
-(defun valid-state (received-state)
75
-  (let* ((session (context :session))
76
-         (saved-state (gethash :state session)))
77
-    (equal saved-state received-state)))
78
-
79
-(defmacro my-with-context-variables ((&rest vars) &body body)
80
-  "This improves fukamachi's version by permitting the variable to be stored somewhere
81
-   besides the symbol corresponding to the keyword."
82
-  `(symbol-macrolet
83
-       ,(loop for (var key) in (ensure-mapping vars)
84
-              for form = `(context ,(intern (string key) :keyword))
85
-              collect `(,var ,form))
86
-     ,@body))
87
-
88
-(defparameter *oid* (make-instance 'ningle:<app>))
89
-(setf drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
90
-
91
-(setf =service-info= (object :parents '()
92
-                             :properties '((client-id nil :accessor client-id)
93
-                                           (secret nil :accessor secret))))
94
-
95
-(setf =endpoint-schema= (object :parents '()
96
-                                :properties '((auth-endpoint nil :accessor auth-endpoint)
97
-                                              (token-endpoint nil :accessor token-endpoint)
98
-                                              (userinfo-endpoint nil :accessor t)
99
-                                              (auth-scope "openid profile email" :accessor t)
100
-                                              (redirect-uri nil :accessor t))))
101
-
102
-(sheeple:defmessage get-user-info (a b))
103
-(sheeple:defmessage get-access-token (a b))
104
-
105
-(sheeple:defreply get-user-info ((a =endpoint-schema=) (b sheeple:=string=)))
106
-(sheeple:defreply get-access-token ((a =endpoint-schema=) (b sheeple:=string=)))
107
-
108
-(defparameter *endpoint-schema* nil)
109
-(defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal))
110
-(defmacro assoc-cdr (key alist &optional (test '#'eql)) `(cdr (assoc ,key ,alist :test ,test)))
111
-
112
-(defun discover-endpoints (schema discovery-doc-url &key (gat nil gat-p) (gui nil gui-p))
113
-  "Discover endpoints on the basis of a discovery document stored at a particular url.
114
-   The two keyword arguments define a function to bind to sheeple replies for get-user-token
115
-   and get-access-token."
116
-  (prog1 schema
117
-    (let ((discovery-document (cl-json:decode-json-from-string (drakma:http-request discovery-doc-url))))
118
-      (setf (auth-endpoint schema) (assoc-cdr :authorization--endpoint discovery-document)
119
-            (token-endpoint schema) (assoc-cdr :token--endpoint discovery-document)
120
-            (userinfo-endpoint schema) (assoc-cdr :userinfo--endpoint discovery-document))
121
-      (when gui-p
122
-        (format t "defining gui-p")
123
-        (sheeple:defreply get-user-info ((a schema)) (funcall gui a)))
124
-      (when gat-p
125
-        (format t "defining gat-p")
126
-        (sheeple:defreply get-access-token ((a schema) (b sheeple:=string=))
127
-          (funcall gat a b))))))
128
-
129
-(defun do-auth-request (endpoint-schema state)
130
-  (drakma:http-request (auth-endpoint endpoint-schema)
131
-                       :redirect nil
132
-                       :parameters `(("client_id" . ,(client-id endpoint-schema))
133
-                                     ("app_id" . ,(client-id endpoint-schema))
134
-                                     ("response_type" . "code")
135
-                                     ("scope" . ,(auth-scope endpoint-schema))
136
-                                     ("redirect_uri" . ,(redirect-uri endpoint-schema))
137
-                                     ("state" . ,state))))
138
-
139
-(defmacro auth-entry-point (name endpoint-schema)
140
-  `(defun ,name (params)
141
-     (declare (ignore params))
142
-     (with-session-values (state endpoint-schema) (context :session)
143
-       (setf state (gen-state 36)
144
-             endpoint-schema ,endpoint-schema)
145
-       (with-endpoints ,endpoint-schema
146
-         (multiple-value-bind (content rcode headers uri) (do-auth-request ,endpoint-schema state)
147
-           (declare (ignore headers))
148
-           (if (< rcode 400) `(302 (:location ,(format nil "~a" uri)))
149
-             content))))))
150
-
151 40
 (defun run-callback-function (endpoint-schema params get-app-user-cb get-login-data)
152 41
   (flet ((get-code (params) (assoc-cdr "code" params #'equal)))
153 42
     (let ((a-t (get-access-token endpoint-schema (get-code params))))
154
-      (auth-callback-skeleton params (:endpoint-schema endpoint-schema
155
-                                      :auth-session-vars (accesstoken userinfo idtoken app-user))
156
-        (multiple-value-bind (access-token user-info id-token) (funcall get-login-data a-t)
43
+      (multiple-value-bind (access-token user-info id-token) (funcall get-login-data a-t)
44
+        (auth-callback-skeleton params (:endpoint-schema endpoint-schema
45
+                                        :auth-session-vars (accesstoken userinfo idtoken app-user))
157 46
           (setf accesstoken access-token
158 47
                 userinfo user-info
159 48
                 idtoken id-token
160 49
                 app-user (funcall get-app-user-cb user-info id-token access-token)))
161 50
         '(302 (:location "/"))))))
162 51
 
163
-(defmacro generate-auth-callback (name endpoint-schema params &body body)
164
-  (with-gensyms (get-app-user-cb cb-params)
165
-    `(defun ,name (,get-app-user-cb)
166
-       (lambda (,cb-params)
167
-         (run-callback-function
168
-           ,endpoint-schema ,cb-params ,get-app-user-cb
169
-           (lambda ,params
170
-             ,@body))))))
171
-
172
-(defmacro reject-when-state-invalid (params &body body)
173
-  (alexandria:with-gensyms (received-state)
174
-    (alexandria:once-only (params)
175
-      `(let ((,received-state (cdr (string-assoc "state" ,params))))
176
-         (if (not (valid-state ,received-state))
177
-           '(403 '() "Out, vile imposter!")
178
-        ,@body)))))
179
-
180
-(defmacro auth-callback-skeleton (params (&key endpoint-schema auth-session-vars) &body body)
181
-  (alexandria:with-gensyms (session)
182
-    (alexandria:once-only (params endpoint-schema)
183
-      `(reject-when-state-invalid ,params
184
-         (with-endpoints ,endpoint-schema
185
-           (my-with-context-variables ((,session session))
186
-             ,(if (null auth-session-vars)
187
-                `(progn
188
-                   ,@body)
189
-                `(with-session-values ,auth-session-vars ,session
190
-                   ,@body))))))))
191
-
192 52
 (define-condition user-not-logged-in (error) ())
193 53
 
194
-(defmacro ensure-logged-in (&body body)
195
-  "Ensure that the user is logged in: otherwise throw the condition user-not-logged-in"
196
-  (alexandria:with-gensyms (session userinfo)
197
-    `(my-with-context-variables ((,session session))
198
-       (with-session-values ((,userinfo userinfo)) ,session
199
-         (if (null ,userinfo)
200
-           (error 'user-not-logged-in)
201
-           (progn ,@body))))))
202
-
203
-(flet ((handle-no-user (main-body handler-body)
204
-         `(handler-case (ensure-logged-in ,@main-body)
205
-            (user-not-logged-in (e) (declare (ignorable e))
206
-                                ,@handler-body))))
207
-
208
-  (defmacro check-login (&body body)
209
-    "Returns an HTTP 401 Error if not logged in."
210
-    (handle-no-user body `('(401 () "Unauthorized"))))
211 54
 
212
-  (defmacro require-login (&body body)
213
-    "Redirects to /login if not logged in."
214
-    (handle-no-user body
215
-                    `((with-session-values (next-page) (context :session)
216
-                        (setf next-page (lack.request:request-path-info *request*))
217
-                        '(302 (:location "/login")))))))
218 55
 
219
-(defparameter *fbook-info* (sheeple:clone =service-info=))
220
-(defparameter *goog-info* (sheeple:clone =service-info=))
221
-(defparameter *goog-endpoint-schema* (defobject (=endpoint-schema= *goog-info*)))
222 56
 
223
-(defproto *fbook-endpoint-schema* (=endpoint-schema= *fbook-info*)
224
-          ((auth-endpoint "https://www.facebook.com/dialog/oauth")
225
-           (token-endpoint "https://graph.facebook.com/v2.3/oauth/access_token")
226
-           (userinfo-endpoint "https://graph.facebook.com/v2.3/me")
227
-           (auth-scope "email")
228
-           (redirect-uri  "http://srv2.elangley.org:9090/oidc_callback/facebook")))
229
-
230
-(sheeple:defreply get-access-token ((endpoint-schema *fbook-endpoint-schema*) (code sheeple:=string=))
231
-  (cl-json:decode-json-from-string
232
-    (drakma:http-request (token-endpoint endpoint-schema)
233
-                         :method :post
234
-                         :redirect nil
235
-                         :parameters `(("code" . ,code)
236
-                                       ("client_id" . ,(client-id endpoint-schema))
237
-                                       ("app_id" . ,(client-id endpoint-schema))
238
-                                       ("client_secret" . ,(secret endpoint-schema))
239
-                                       ("redirect_uri" . ,(redirect-uri endpoint-schema))
240
-                                       ("grant_type" . "authorization_code")
241
-                                       ("")
242
-                                       ))))
243
-
244
-(sheeple:defreply get-user-info ((endpoint-schema *fbook-endpoint-schema*) (access-token sheeple:=string=))
245
-  (let ((endpoint (userinfo-endpoint endpoint-schema)))
246
-    (cl-json:decode-json-from-string
247
-      (drakma:http-request endpoint
248
-                           :parameters `(("access_token" . ,access-token))))))
249 57
 
250 58
 (defun load-provider-secrets (provider-info secrets)
251 59
   (setf (client-id provider-info) (assoc-cdr :client-id secrets)
... ...
@@ -265,21 +73,13 @@
265 73
 (defun load-goog-endpoint-schema ()
266 74
   (discover-endpoints *goog-endpoint-schema*
267 75
                       "https://accounts.google.com/.well-known/openid-configuration"
268
-                      :gat #'goog-get-access-token)
76
+                      #'goog-get-access-token)
269 77
   (setf (redirect-uri *goog-endpoint-schema*) "http://srv2.elangley.org:9090/oidc_callback/google"))
270 78
 
271
-(sheeple:defreply get-user-info ((endpoint-schema *goog-endpoint-schema*) (access-token sheeple:=string=))
272
-  (format t "getting user data: ~a~%" "blarg")
273
-  (let ((endpoint (userinfo-endpoint endpoint-schema)))
274
-    (cl-json:decode-json-from-string
275
-      (drakma:http-request endpoint
276
-                           :parameters `(("alt" . "json")
277
-                                         ("access_token" . ,access-token))))))
79
+(define-auth-entry-point google-login-entry *goog-endpoint-schema*)
80
+(define-auth-entry-point facebook-login-entry *fbook-endpoint-schema*)
278 81
 
279
-(auth-entry-point google-login-entry *goog-endpoint-schema*)
280
-(auth-entry-point facebook-login-entry *fbook-endpoint-schema*)
281
-
282
-(generate-auth-callback google-callback *goog-endpoint-schema* (a-t)
82
+(define-auth-callback google-callback *goog-endpoint-schema* (a-t)
283 83
   (labels ((get-real-access-token (a-t) (assoc-cdr :access--token a-t))
284 84
            (get-id-token (a-t) (cljwt:decode (assoc-cdr :id--token a-t) :fail-if-unsupported nil)))
285 85
     (let ((access-token (get-real-access-token a-t)))
... ...
@@ -287,7 +87,7 @@
287 87
         (get-user-info *goog-endpoint-schema* access-token)
288 88
         (get-id-token a-t)))))
289 89
 
290
-(generate-auth-callback facebook-callback *fbook-endpoint-schema* (a-t)
90
+(define-auth-callback facebook-callback *fbook-endpoint-schema* (a-t)
291 91
   (labels ((get-id-token (a-t) (assoc-cdr :access--token a-t)))
292 92
     ; ^-- access--token is not a mistake here
293 93
     (let ((id-token (get-id-token a-t)))
... ...
@@ -306,16 +106,3 @@
306 106
         (route app "/oidc_callback/google" :method :get) (google-callback login-callback)
307 107
         (route app "/oidc_callback/facebook" :method :get) (facebook-callback login-callback)))
308 108
 
309
-(defmacro setup-oid-connect (app args &body callback)
310
-  `(bind-oid-connect-routes ,app (lambda ,args ,@callback)))
311
-
312
-(defmacro redirect-if-necessary (sessionvar &body body)
313
-  (with-gensyms (session)
314
-    `(let* ((,session ,sessionvar)
315
-            (next-page (gethash :next-page ,session)))
316
-       (if (and (not (null next-page))
317
-                (not (string= next-page (lack.request:request-path-info *request*))))
318
-         (progn
319
-           (setf (gethash :next-page ,session) nil)
320
-           `(302 (:location ,next-page)))
321
-         ,@body))))
322 109
new file mode 100644
... ...
@@ -0,0 +1,120 @@
1
+#|
2
+ |Copyright (c) 2015 Edward Langley
3
+ |All rights reserved.
4
+ |
5
+ |Redistribution and use in source and binary forms, with or without
6
+ |modification, are permitted provided that the following conditions
7
+ |are met:
8
+ |
9
+ |Redistributions of source code must retain the above copyright notice,
10
+ |this list of conditions and the following disclaimer.
11
+ |
12
+ |Redistributions in binary form must reproduce the above copyright
13
+ |notice, this list of conditions and the following disclaimer in the
14
+ |documentation and/or other materials provided with the distribution.
15
+ |
16
+ |Neither the name of the project's author nor the names of its
17
+ |contributors may be used to endorse or promote products derived from
18
+ |this software without specific prior written permission.
19
+ |
20
+ |THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ |"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ |LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23
+ |FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ |HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ |SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES  INCLUDING, BUT NOT LIMITED
26
+ |TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27
+ |PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28
+ |LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29
+ |NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ |SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ |
32
+ |#
33
+
34
+(in-package :cl-oid-connect.objects)
35
+
36
+(defparameter *oid* (make-instance 'ningle:<app>))
37
+(setf drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
38
+
39
+(setf =service-info= (object :parents '()
40
+                             :properties '((client-id nil :accessor client-id)
41
+                                           (secret nil :accessor secret))))
42
+(defparameter *fbook-info* (clone =service-info=))
43
+(defparameter *goog-info* (clone =service-info=))
44
+
45
+(setf =endpoint-schema= (object :parents '()
46
+                                :properties '((auth-endpoint nil :accessor auth-endpoint)
47
+                                              (token-endpoint nil :accessor token-endpoint)
48
+                                              (userinfo-endpoint nil :accessor t)
49
+                                              (auth-scope "openid profile email" :accessor t)
50
+                                              (redirect-uri nil :accessor t))))
51
+(defparameter *endpoint-schema* nil)
52
+
53
+(defmessage get-user-info (a b))
54
+(defmessage get-access-token (a b))
55
+(defmessage discover-endpoints (a b c))
56
+
57
+(defreply get-user-info ((a =endpoint-schema=) (b =string=)))
58
+(defreply get-access-token ((a =endpoint-schema=) (b =string=)))
59
+
60
+(defreply discover-endpoints ((schema =endpoint-schema=) discovery-doc-url get-access-token)
61
+  "Discover endpoints on the basis of a discovery document stored at a particular url.
62
+   The two keyword arguments define a function to bind to sheeple replies for get-user-token
63
+   and get-access-token."
64
+  (let ((discovery-document (yason:parse (drakma:http-request discovery-doc-url))))
65
+    (setf (auth-endpoint schema)     (gethash "authorization_endpoint" discovery-document)
66
+          (token-endpoint schema)    (gethash "token_endpoint" discovery-document)
67
+          (userinfo-endpoint schema) (gethash "userinfo_endpoint" discovery-document))
68
+    (defreply get-access-token ((a schema) (b =string=))
69
+      (funcall get-access-token a b))
70
+
71
+    schema))
72
+
73
+(defparameter *goog-endpoint-schema* (defobject (=endpoint-schema= *goog-info*)))
74
+
75
+(defreply get-user-info ((endpoint-schema *goog-endpoint-schema*) (access-token =string=))
76
+  (format t "getting user data: ~a~%" "blarg")
77
+  (let ((endpoint (userinfo-endpoint endpoint-schema)))
78
+    (cl-json:decode-json-from-string
79
+      (drakma:http-request endpoint
80
+                           :parameters `(("alt" . "json")
81
+                                         ("access_token" . ,access-token))))))
82
+
83
+
84
+(defproto *fbook-endpoint-schema* (=endpoint-schema= *fbook-info*)
85
+          ((auth-endpoint "https://www.facebook.com/dialog/oauth")
86
+           (token-endpoint "https://graph.facebook.com/v2.3/oauth/access_token")
87
+           (userinfo-endpoint "https://graph.facebook.com/v2.3/me")
88
+           (auth-scope "email")
89
+           (redirect-uri  "http://srv2.elangley.org:9090/oidc_callback/facebook")))
90
+
91
+
92
+(defreply get-access-token ((endpoint-schema *fbook-endpoint-schema*) (code =string=))
93
+  (cl-json:decode-json-from-string
94
+    (drakma:http-request (token-endpoint endpoint-schema)
95
+                         :method :post
96
+                         :redirect nil
97
+                         :parameters `(("code" . ,code)
98
+                                       ("client_id" . ,(client-id endpoint-schema))
99
+                                       ("app_id" . ,(client-id endpoint-schema))
100
+                                       ("client_secret" . ,(secret endpoint-schema))
101
+                                       ("redirect_uri" . ,(redirect-uri endpoint-schema))
102
+                                       ("grant_type" . "authorization_code")))))
103
+
104
+(defreply get-user-info ((endpoint-schema *fbook-endpoint-schema*) (access-token =string=))
105
+  (let ((endpoint (userinfo-endpoint endpoint-schema)))
106
+    (cl-json:decode-json-from-string
107
+      (drakma:http-request endpoint
108
+                           :parameters `(("access_token" . ,access-token))))))
109
+
110
+(defun do-auth-request (endpoint-schema state)
111
+  (drakma:http-request (auth-endpoint endpoint-schema)
112
+                       :redirect nil
113
+                       :parameters `(("client_id" . ,(client-id endpoint-schema))
114
+                                     ("app_id" . ,(client-id endpoint-schema))
115
+                                     ("response_type" . "code")
116
+                                     ("scope" . ,(auth-scope endpoint-schema))
117
+                                     ("redirect_uri" . ,(redirect-uri endpoint-schema))
118
+                                     ("state" . ,state))))
119
+
120
+
... ...
@@ -1,32 +1,23 @@
1 1
 ;;;; package.lisp
2 2
 
3
-(defpackage :cl-oid-connect
3
+(defpackage #:cl-oid-connect.utils
4
+  (:use #:cl #:alexandria #:anaphora #:fwoar.lisputils)
5
+  (:export #:vars-to-symbol-macrolets #:with-session-values #:with-endpoints
6
+           #:with-session #:def-route #:gen-state #:valid-state #:my-with-context-variables
7
+           #:string-assoc #:assoc-cdr #:define-auth-entry-point #:define-auth-callback
8
+           #:reject-when-state-invalid #:auth-callback-skeleton #:ensure-logged-in
9
+           #:setup-oid-connect #:check-login #:require-login #:redirect-if-necessary))
10
+
11
+(defpackage #:cl-oid-connect.objects
12
+  (:use #:cl #:alexandria #:anaphora #:fwoar.lisputils #:cl-oid-connect.utils #:sheeple))
13
+
14
+(defpackage #:cl-oid-connect
4 15
   (:use
5
-    #:cl
6
-    #:alexandria
7
-    #:anaphora
8
-    #:clack
9
-    #:cl-json
10
-    #:cljwt
11
-    #:cl-who
12
-    #:drakma
16
+    #:cl #:alexandria #:anaphora #:clack #:cl-json #:cljwt #:cl-who #:drakma
13 17
     ;#:lack-middleware-session
14
-    #:iterate
15
-    #:ningle
16
-    #:lquery
17
-    #:plump
18
-    #:sheeple
19
-    #:whitespace.utils
20
-    )
18
+    #:iterate #:ningle #:lquery #:plump #:sheeple #:fwoar.lisputils
19
+    #:cl-oid-connect.objects #:cl-oid-connect.utils)
21 20
   (:export
22
-    #:redirect-if-necessary
23
-    #:def-route
24
-    #:require-login
25
-    #:oauth2-login-middleware
26
-    #:with-session
27
-    #:assoc-cdr
28
-    #:session ; private!!
29
-    #:vars-to-symbol-macrolets
30
-    #:initialize-oid-connect
31
-    ))
21
+    #:redirect-if-necessary #:def-route #:require-login #:oauth2-login-middleware #:with-session
22
+    #:assoc-cdr #:session #| private!! |# #:vars-to-symbol-macrolets #:initialize-oid-connect))
32 23
 
... ...
@@ -1,77 +1,167 @@
1
-(defpackage whitespace.utils
2
-  (:use #:cl #:alexandria #:iterate))
3
-
4
-(in-package whitespace.utils)
5
-
6
-(defun ensure-mapping (list)
7
-  "Make sure that each item of the list is a pair of symbols"
8
-  (mapcar (lambda (x) (if (symbolp x) (list x x) x)) list))
9
-(export 'ensure-mapping)
10
-
11
-(defun alist-string-hash-table (alist)
12
-  (alexandria:alist-hash-table alist :test #'string=))
13
-(export 'alist-string-hash-table)
14
-
15
-(defun make-pairs (symbols)
16
-  (cons 'list (iterate (for (key value) in symbols)
17
-                       (collect (list 'list* (symbol-name key) value)))))
18
-(export 'make-pairs)
19
-
20
-(defmacro copy-slots (slots from-v to-v)
21
-  (with-gensyms (from to)
22
-    `(let ((,from ,from-v) (,to ,to-v))
23
-       ,@(iterate (for (fro-slot to-slot) in (ensure-mapping slots))
24
-                  (collect `(setf (slot-value ,to ',to-slot) (slot-value ,from ',fro-slot))))
25
-       ,to)))
26
-(export 'copy-slots)
27
-
28
-
29
-(defun transform-alist (pair-transform alist)
30
-  (iterate (for (k . v) in-sequence alist)
31
-           (collect
32
-             (funcall pair-transform k v))))
33
-(export 'transform-alist)
34
-
35
-(defun %json-pair-transform (k v)
36
-  (cons (make-keyword (string-downcase k))
37
-        (typecase v
38
-          (string (coerce v 'simple-string))
39
-          (t v))))
40
-(export '%json-pair-transform)
41
-
42
-(defun %default-pair-transform (k v)
43
-  (cons (make-keyword (string-upcase k)) v))
44
-(export '%default-pair-transform)
45
-
46
-(defmacro default-when (default test &body body)
47
-  (once-only (default)
48
-    `(or (when ,test
49
-           ,@body)
50
-         ,default)))
51
-(export 'default-when)
52
-
53
-(defmacro transform-result ((list-transform pair-transform) &body alist)
54
-  `(funcall ,list-transform
55
-            (transform-alist ,pair-transform
56
-                             ,@alist)))
57
-(export 'transform-result)
58
-
59
-
60
-(defmacro slots-to-pairs (obj (&rest slots))
61
-  (once-only (obj)
62
-    (let* ((slots (ensure-mapping slots))
63
-           (bindings (iterate (for (slot v &key bind-from) in slots)
64
-                              (collect (or bind-from slot)))))
65
-      `(with-slots ,bindings ,obj
66
-         ,(make-pairs slots)))))
67
-(export 'slots-to-pairs)
68
-
69
-(defun normalize-html (html)
70
-  (let ((plump-parser:*tag-dispatchers* plump:*html-tags*))
71
-    (with-output-to-string (ss)
72
-      (prog1 ss
73
-        (map 'vector
74
-           (lambda (x) (plump:serialize (plump:parse (plump:text x)) ss))
75
-           html)))))
76
-(export 'normalize-html)
1
+#|
2
+ |Copyright (c) 2015 Edward Langley
3
+ |All rights reserved.
4
+ |
5
+ |Redistribution and use in source and binary forms, with or without
6
+ |modification, are permitted provided that the following conditions
7
+ |are met:
8
+ |
9
+ |Redistributions of source code must retain the above copyright notice,
10
+ |this list of conditions and the following disclaimer.
11
+ |
12
+ |Redistributions in binary form must reproduce the above copyright
13
+ |notice, this list of conditions and the following disclaimer in the
14
+ |documentation and/or other materials provided with the distribution.
15
+ |
16
+ |Neither the name of the project's author nor the names of its
17
+ |contributors may be used to endorse or promote products derived from
18
+ |this software without specific prior written permission.
19
+ |
20
+ |THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ |"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ |LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23
+ |FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ |HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ |SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES  INCLUDING, BUT NOT LIMITED
26
+ |TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27
+ |PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28
+ |LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29
+ |NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ |SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ |
32
+ |#
33
+
34
+(in-package :cl-oid-connect.utils)
35
+(eval-when (:compile-toplevel :execute :load-toplevel)
36
+  (defun vars-to-symbol-macrolets (vars obj)
37
+    (iterate:iterate (iterate:for (store key) in (ensure-mapping vars))
38
+                     (iterate:collect `(,store (gethash ,(alexandria:make-keyword key) ,obj))))))
39
+
40
+(defmacro with-session-values (vars session &body body)
41
+  (alexandria:once-only (session)
42
+    `(symbol-macrolet ,(vars-to-symbol-macrolets vars session)
43
+       ,@body)))
44
+
45
+; This probably should eventually go?
46
+(defmacro with-endpoints (endpoint-schema  &body body)
47
+  `(let* ((*endpoint-schema* ,endpoint-schema))
48
+     ,@body))
49
+
50
+(defmacro with-session ((var) &body body)
51
+  `(progn
52
+     (format t "The session var is: ~a it contains: ~a~%"  ,(symbol-name var) ,var)
53
+     (let ((,var (context :session)))
54
+       (format t "The session var is: ~a it now contains: ~a~%"  ,(symbol-name var) ,var)
55
+       ,@body)))
56
+
57
+(defmacro def-route ((url args &key (app *oid*) (method :GET)) &body body)
58
+  `(setf (ningle:route ,app ,url :method ,method)
59
+         (lambda ,args
60
+           (declare (ignorable ,@args))
61
+           ,@body)))
62
+(defun gen-state (len)
63
+  (with-output-to-string (stream)
64
+    (let ((*print-base* 36))
65
+      (loop repeat len
66
+            do (princ (random 36) stream)))))
67
+
68
+(defun valid-state (received-state)
69
+  (let* ((session (context :session))
70
+         (saved-state (gethash :state session)))
71
+    (equal saved-state received-state)))
72
+
73
+(defmacro my-with-context-variables ((&rest vars) &body body)
74
+  "This improves fukamachi's version by permitting the variable to be stored somewhere
75
+   besides the symbol corresponding to the keyword."
76
+  `(symbol-macrolet
77
+       ,(loop for (var key) in (ensure-mapping vars)
78
+              for form = `(context ,(intern (string key) :keyword))
79
+              collect `(,var ,form))
80
+     ,@body))
81
+
82
+(defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal))
83
+(defmacro assoc-cdr (key alist &optional (test '#'eql)) `(cdr (assoc ,key ,alist :test ,test)))
84
+
85
+(defmacro define-auth-entry-point (name endpoint-schema)
86
+  `(defun ,name (params)
87
+     (declare (ignore params))
88
+     (with-session-values (state endpoint-schema) (context :session)
89
+       (setf state (gen-state 36)
90
+             endpoint-schema ,endpoint-schema)
91
+       (with-endpoints ,endpoint-schema
92
+         (multiple-value-bind (content rcode headers uri) (do-auth-request ,endpoint-schema state)
93
+           (declare (ignore headers))
94
+           (if (< rcode 400) `(302 (:location ,(format nil "~a" uri)))
95
+             content))))))
96
+
97
+(defmacro define-auth-callback (name endpoint-schema params &body body)
98
+  (with-gensyms (get-app-user-cb cb-params)
99
+    `(defun ,name (,get-app-user-cb)
100
+       (lambda (,cb-params)
101
+         (run-callback-function
102
+           ,endpoint-schema ,cb-params ,get-app-user-cb
103
+           (lambda ,params
104
+             ,@body))))))
105
+
106
+(defmacro reject-when-state-invalid (params &body body)
107
+  (alexandria:with-gensyms (received-state)
108
+    (alexandria:once-only (params)
109
+      `(let ((,received-state (cdr (string-assoc "state" ,params))))
110
+         (if (not (valid-state ,received-state))
111
+           '(403 '() "Out, vile imposter!")
112
+        ,@body)))))
113
+
114
+(defmacro auth-callback-skeleton (params (&key endpoint-schema auth-session-vars) &body body)
115
+  (alexandria:with-gensyms (session)
116
+    (alexandria:once-only (params endpoint-schema)
117
+      `(reject-when-state-invalid ,params
118
+         (with-endpoints ,endpoint-schema
119
+           (my-with-context-variables ((,session session))
120
+             ,(if (null auth-session-vars)
121
+                `(progn
122
+                   ,@body)
123
+                `(with-session-values ,auth-session-vars ,session
124
+                   ,@body))))))))
125
+
126
+(defmacro ensure-logged-in (&body body)
127
+  "Ensure that the user is logged in: otherwise throw the condition user-not-logged-in"
128
+  (alexandria:with-gensyms (session userinfo)
129
+    `(my-with-context-variables ((,session session))
130
+       (with-session-values ((,userinfo userinfo)) ,session
131
+         (handler-case
132
+           (if (null ,userinfo)
133
+             (error 'user-not-logged-in)
134
+             (progn ,@body))
135
+           (error (c)
136
+             (setf ,userinfo nil)
137
+             (error c)))))))
138
+
139
+(defmacro setup-oid-connect (app args &body callback)
140
+  `(bind-oid-connect-routes ,app (lambda ,args ,@callback)))
141
+
142
+(flet ((handle-no-user (main-body handler-body)
143
+         `(handler-case (ensure-logged-in ,@main-body)
144
+            (user-not-logged-in (e) (declare (ignorable e))
145
+                                ,@handler-body))))
146
+
147
+  (defmacro check-login (&body body)
148
+    "Returns an HTTP 401 Error if not logged in."
149
+    (handle-no-user body `('(401 () "Unauthorized"))))
150
+
151
+  (defmacro require-login (&body body)
152
+    "Redirects to /login if not logged in."
153
+    (handle-no-user body
154
+                    `((with-session-values (next-page) (context :session)
155
+                        (setf next-page (lack.request:request-path-info *request*))
156
+                        '(302 (:location "/login")))))))
157
+
158
+(defmacro redirect-if-necessary (sessionvar &body body)
159
+  (with-gensyms (session)
160
+    `(let* ((,session ,sessionvar)
161
+            (next-page (gethash :next-page ,session)))
162
+       (if (and (not (null next-page))
163
+                (not (string= next-page (lack.request:request-path-info *request*))))
164
+         (progn
165
+           (setf (gethash :next-page ,session) nil)
166
+           `(302 (:location ,next-page)))))))
77 167