git.fiddlerwoaroof.com
Browse code

Cleaning up the auth module

- reorganizing the utility macros + removing unused ones

- redoing the login checks to use the condition system

- Cleaning up the oid entrypoints and callbacks

fiddlerwoaroof authored on 17/10/2015 06:46:04
Showing 1 changed files
... ...
@@ -36,16 +36,34 @@
36 36
 (in-package :cl-oid-connect)
37 37
 ; Should this be here?
38 38
 
39
-(eval-when (:compile-toplevel :execute)
39
+(eval-when (:compile-toplevel :execute :load-toplevel)
40 40
   (defun vars-to-symbol-macrolets (vars obj)
41
-    (iterate:iterate (iterate:for var in vars)
42
-                     (iterate:collect `(,var (gethash ,(alexandria:make-keyword var) ,obj))))))
41
+    (iterate:iterate (iterate:for (store key) in (ensure-mapping vars))
42
+                     (iterate:collect `(,store (gethash ,(alexandria:make-keyword key) ,obj))))))
43 43
 
44 44
 (defmacro with-session-values (vars session &body body)
45 45
   (alexandria:once-only (session)
46 46
     `(symbol-macrolet ,(vars-to-symbol-macrolets vars session)
47 47
        ,@body)))
48 48
 
49
+; This probably should eventually go?
50
+(defmacro with-endpoints (endpoint-schema  &body body)
51
+  `(let* ((*endpoint-schema* ,endpoint-schema))
52
+     ,@body))
53
+
54
+(defmacro with-session ((var) &body body)
55
+  `(progn
56
+     (format t "The session var is: ~a it contains: ~a~%"  ,(symbol-name var) ,var)
57
+     (let ((,var (context :session)))
58
+       (format t "The session var is: ~a it now contains: ~a~%"  ,(symbol-name var) ,var)
59
+       ,@body)))
60
+
61
+(defmacro def-route ((url args &key (app *oid*) (method :GET)) &body body)
62
+  `(setf (ningle:route ,app ,url :method ,method)
63
+         #'(lambda ,args
64
+             (declare (ignorable ,@args))
65
+             ,@body)))
66
+
49 67
 (defparameter *oid* (make-instance 'ningle:<app>))
50 68
 (setf drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
51 69
 
... ...
@@ -70,9 +88,6 @@
70 88
 (defparameter *endpoint-schema* nil)
71 89
 (defparameter *goog-endpoint-schema* (defobject (=endpoint-schema= *goog-info*)))
72 90
 
73
-(defun get-base-url (request)
74
-  (format nil "~a//~a/oidc_callback" (lack.request:request-query-parameters)))
75
-
76 91
 (defproto *fbook-endpoint-schema* (=endpoint-schema= *fbook-info*)
77 92
           ((auth-endpoint "https://www.facebook.com/dialog/oauth")
78 93
            (token-endpoint "https://graph.facebook.com/v2.3/oauth/access_token")
... ...
@@ -104,25 +119,23 @@
104 119
 (defmacro assoc-cdr (key alist &optional (test '#'eql)) `(cdr (assoc ,key ,alist :test ,test)))
105 120
 
106 121
 (defun discover-endpoints (schema discovery-doc-url &key (gat nil gat-p) (gui nil gui-p))
107
-  (let ((discovery-document (cl-json:decode-json-from-string (drakma:http-request discovery-doc-url))))
108
-
109
-    (setf (auth-endpoint schema) (assoc-cdr :authorization--endpoint discovery-document))
110
-    (setf (token-endpoint schema) (assoc-cdr :token--endpoint discovery-document))
111
-    (setf (userinfo-endpoint schema) (assoc-cdr :userinfo--endpoint discovery-document))
112
-
113
-    (if gui-p (sheeple:defreply get-user-info ((a schema)) (funcall gui a)))
114
-    (if gat-p (sheeple:defreply get-access-token ((a schema) (b sheeple:=string=))
115
-                (funcall gat a b)))
116
-
117
-    schema))
118
-
119
-; This probably should eventually go?
120
-(defmacro with-endpoints (endpoint-schema  &body body)
121
-  `(let* ((*endpoint-schema* ,endpoint-schema))
122
-     ,@body))
122
+  "Discover endpoints on the basis of a discovery document stored at a particular url.
123
+   The two keyword arguments define a function to bind to sheeple replies for get-user-token
124
+   and get-access-token."
125
+  (prog1 schema
126
+    (let ((discovery-document (cl-json:decode-json-from-string (drakma:http-request discovery-doc-url))))
127
+      (setf (auth-endpoint schema) (assoc-cdr :authorization--endpoint discovery-document)
128
+            (token-endpoint schema) (assoc-cdr :token--endpoint discovery-document)
129
+            (userinfo-endpoint schema) (assoc-cdr :userinfo--endpoint discovery-document))
130
+      (when gui-p
131
+        (format t "defining gui-p")
132
+        (sheeple:defreply get-user-info ((a schema)) (funcall gui a)))
133
+      (when gat-p
134
+        (format t "defining gat-p")
135
+        (sheeple:defreply get-access-token ((a schema) (b sheeple:=string=))
136
+          (funcall gat a b))))))
123 137
 
124 138
 (defun do-auth-request (endpoint-schema state)
125
-  (format t "~%client-id: ~a~%" (auth-endpoint endpoint-schema))
126 139
   (drakma:http-request (auth-endpoint endpoint-schema)
127 140
                        :redirect nil
128 141
                        :parameters `(("client_id" . ,(client-id endpoint-schema))
... ...
@@ -139,44 +152,49 @@
139 152
             do (princ (random 36) stream)))))
140 153
 
141 154
 
142
-(defmacro def-route ((url args &key (app *oid*) (method :GET)) &body body)
143
-  `(setf (ningle:route ,app ,url :method ,method)
144
-         #'(lambda ,args
145
-             (declare (ignorable ,@args))
146
-             ,@body)))
155
+(defun valid-state (received-state)
156
+  (let* ((session (context :session))
157
+         (saved-state (gethash :state session)))
158
+    (equal saved-state received-state)))
147 159
 
148
-(defmacro check-state (received-state then else)
149
-  (alexandria:with-gensyms (saved-state session)
150
-    `(let* ((,session (context :session))
151
-            (,saved-state (gethash :state ,session)))
152
-       (if (equal ,saved-state ,received-state)
153
-         ,then
154
-         ,else))))
160
+(define-condition user-not-logged-in (error) ())
155 161
 
156
-(defmacro check-login (&body body)
157
-  (alexandria:with-gensyms (session)
158
-    `(let ((,session (context :session)))
159
-       (if (not (eql nil (gethash :userinfo ,session)))
160
-         (progn ,@body)
161
-         (progn
162
-           (setf (gethash :next-page ,session) (lack.request:request-path-info *request*))
163
-           '(401 () "Unauthorized"))))))
162
+(defmacro my-with-context-variables ((&rest vars) &body body)
163
+  "This improves fukamachi's version by permitting the variable to be stored somewhere
164
+   besides the symbol corresponding to the keyword."
165
+  `(symbol-macrolet
166
+       ,(loop for (var key) in (ensure-mapping vars)
167
+              for form = `(context ,(intern (string key) :keyword))
168
+              collect `(,var ,form))
169
+     ,@body))
164 170
 
165
-(defmacro require-login (&body body)
166
-  (alexandria:with-gensyms (session)
167
-    `(let ((,session (context :session)))
168
-       (if (not (eql nil (gethash :userinfo ,session)))
169
-         (progn ,@body)
170
-         (progn
171
-           (setf (gethash :next-page ,session) (lack.request:request-path-info *request*))
172
-           '(302 (:location "/login")))))))
173
-
174
-(defmacro with-session ((var) &body body)
175
-  `(progn
176
-     (format t "The session var is: ~a it contains: ~a~%"  ,(symbol-name var) ,var)
177
-     (let ((,var (context :session)))
178
-       (format t "The session var is: ~a it now contains: ~a~%"  ,(symbol-name var) ,var)
179
-       ,@body)))
171
+(defmacro ensure-logged-in (&body body)
172
+  "Ensure that the user is logged in: otherwise throw the condition user-not-logged-in"
173
+  (alexandria:with-gensyms (session userinfo)
174
+    `(my-with-context-variables ((,session session))
175
+       (with-session-values ((,userinfo userinfo)) ,session
176
+         (if (null ,userinfo)
177
+           (error 'user-not-logged-in)
178
+           (progn ,@body))))))
179
+
180
+(flet
181
+  ((handle-no-user (main-body handler-body)
182
+     `(handler-case
183
+        (ensure-logged-in ,@main-body)
184
+        (user-not-logged-in (e)
185
+                            (declare (ignorable e))
186
+                            ,@handler-body))))
187
+
188
+  (defmacro check-login (&body body)
189
+    "Returns an HTTP 401 Error if not logged in."
190
+    (handle-no-user body `('(401 () "Unauthorized"))))
191
+
192
+  (defmacro require-login (&body body)
193
+    "Redirects to /login if not logged in."
194
+    (handle-no-user body
195
+                    `((with-session-values (next-page) (context :session)
196
+                        (setf next-page (lack.request:request-path-info *request*))
197
+                        '(302 (:location "/login")))))))
180 198
 
181 199
 (defun load-facebook-info (loadfrom)
182 200
   (with-open-file (fbook-info (truename loadfrom))
... ...
@@ -209,7 +227,7 @@
209 227
   (discover-endpoints *goog-endpoint-schema*
210 228
                       "https://accounts.google.com/.well-known/openid-configuration"
211 229
                       :gat #'goog-get-access-token)
212
-  (setf (redirect-uri *goog-endpoint-schema*)   "http://srv2.elangley.org:9090/oidc_callback/google"))
230
+  (setf (redirect-uri *goog-endpoint-schema*) "http://srv2.elangley.org:9090/oidc_callback/google"))
213 231
 
214 232
 (sheeple:defreply get-user-info ((endpoint-schema *goog-endpoint-schema*) (access-token sheeple:=string=))
215 233
   (format t "getting user data: ~a~%" "blarg")
... ...
@@ -217,81 +235,73 @@
217 235
     (cl-json:decode-json-from-string
218 236
       (drakma:http-request endpoint
219 237
                            :parameters `(("alt" . "json")
220
-                                         ("access_token" . ,access-token))
221
-                           ))))
238
+                                         ("access_token" . ,access-token))))))
239
+(defmacro auth-entry-point (name endpoint-schema)
240
+  `(defun ,name (params)
241
+     (declare (ignore params))
242
+     (with-session-values (state endpoint-schema) (context :session)
243
+       (setf state (gen-state 36)
244
+             endpoint-schema ,endpoint-schema)
245
+       (with-endpoints ,endpoint-schema
246
+         (multiple-value-bind (content rcode headers uri) (do-auth-request ,endpoint-schema state)
247
+           (declare (ignore headers))
248
+           (if (< rcode 400) `(302 (:location ,(format nil "~a" uri)))
249
+             content))))))
250
+
251
+(auth-entry-point google-login-entry *goog-endpoint-schema*)
252
+(auth-entry-point facebook-login-entry *fbook-endpoint-schema*)
253
+
254
+(defmacro def-callback-generator (name generator-args callback-args &body body)
255
+  `(defun ,name ,generator-args
256
+     (lambda ,callback-args
257
+       ,@body)))
222 258
 
223
-(defun google-login-entry (params)
224
-  (declare (ignore params))
225
-  (with-context-variables (session)
226
-    (let ((state (gen-state 36)))
227
-      (setf (gethash :state session) state)
228
-      (with-endpoints *goog-endpoint-schema*
229
-        (setf (gethash :endpoint-schema session) *goog-endpoint-schema*)
230
-        (multiple-value-bind (content rcode headers) (do-auth-request *goog-endpoint-schema* state)
231
-          (if (< rcode 400)
232
-            `(302 (:location ,(cdr (assoc :location headers))))
233
-            content))))))
234
-
235
-(defun facebook-login-entry (params)
236
-  (declare (ignore params))
237
-  (let ((session (ningle:context :session))
238
-        (state (gen-state 36)))
239
-      (setf (gethash :state session) state)
240
-      (with-endpoints *fbook-endpoint-schema*
241
-        (setf (gethash :endpoint-schema session) *fbook-endpoint-schema*)
242
-        (multiple-value-bind (content rcode headers uri) (do-auth-request *fbook-endpoint-schema* state)
243
-          (declare (ignore headers))
244
-          (if (< rcode 400)
245
-            `(302 (:location ,(format nil "~a" uri)))
246
-            content)))))
247
-
248
-(defun google-callback (login-callback)
249
-  (lambda (params)
250
-    (let ((received-state (cdr (string-assoc "state" params)))
251
-          (code (cdr (string-assoc "code" params))))
252
-      (check-state received-state
253
-                   (with-context-variables (session)
254
-                     (with-endpoints *goog-endpoint-schema*
255
-                       (let* ((a-t (get-access-token *goog-endpoint-schema* code))
256
-                              (access-token (assoc-cdr :access--token a-t)) ;; Argh
257
-                              (id-token (assoc-cdr :id--token a-t))
258
-                              (decoded (cljwt:decode id-token :fail-if-unsupported nil))
259
-                              (user-info (get-user-info *goog-endpoint-schema* access-token)))
260
-                         (setf (gethash :idtoken session) id-token
261
-                               (gethash :accesstoken session) access-token
262
-                               (gethash :userinfo session) user-info
263
-                               (gethash :app-user session) (funcall login-callback
264
-                                                                    user-info
265
-                                                                    decoded
266
-                                                                    access-token))
267
-                         '(302 (:location "/"))
268
-                         )))
269
-                   '(403 '() "Out, vile imposter!")))))
270
-
271
-(defmacro setup-session ((session) &rest rest &key nonsense &allow-other-keys)
272
-  (declare (ignorable nonsense))
273
-  (cons 'progn
274
-        (iterate:iterate (iterate:for key   in rest       by #'cddr )
275
-                         (iterate:for value in (cdr rest) by #'cddr)
276
-                         (iterate:collect `(setf (gethash ,(alexandria:make-keyword (key)) ,session) ,value)))))
277
-
278
-(defun facebook-callback (login-callback)
279
-  (lambda (params)
280
-    (let ((received-state (cdr (string-assoc "state" params)))
281
-          (code (cdr (string-assoc "code" params))))
282
-      (with-endpoints *fbook-endpoint-schema*
283
-        (check-state received-state
284
-                     (let* ((a-t (get-access-token *fbook-endpoint-schema* code))
285
-                            (id-token (assoc-cdr :access--token a-t))
286
-                            (user-info (get-user-info *fbook-endpoint-schema* id-token)))
287
-                       (with-session-values (accesstoken userinfo idtoken app-user) (context :session)
288
-                                            (setf accesstoken a-t
289
-                                                  userinfo user-info
290
-                                                  idtoken id-token
291
-                                                  app-user (funcall login-callback user-info id-token a-t)))
292
-
293
-                         '(302 (:location "/")))
294
-                     '(403 '() "Out, vile imposter!"))))))
259
+(defmacro reject-when-state-invalid (params &body body)
260
+  (alexandria:with-gensyms (received-state)
261
+    (alexandria:once-only (params)
262
+      `(let ((,received-state (cdr (string-assoc "state" ,params))))
263
+         (if (not (valid-state ,received-state))
264
+           '(403 '() "Out, vile imposter!")
265
+        ,@body)))))
266
+
267
+(defmacro auth-callback-skeleton (params (&key endpoint-schema auth-session-vars) &body body)
268
+  (alexandria:with-gensyms (session)
269
+    (alexandria:once-only (params endpoint-schema)
270
+      `(reject-when-state-invalid ,params
271
+         (with-endpoints ,endpoint-schema
272
+           (my-with-context-variables ((,session session))
273
+             (with-session-values ,auth-session-vars ,session
274
+               ,@body)))))))
275
+
276
+(flet ((get-code (params) (assoc-cdr "code" params #'equal)))
277
+
278
+  (def-callback-generator google-callback (get-app-user-cb) (params)
279
+    (auth-callback-skeleton params (:endpoint-schema *goog-endpoint-schema*
280
+                                    :auth-session-vars (accesstoken userinfo idtoken app-user))
281
+      (flet ((get-real-access-token (a-t) (assoc-cdr :access--token a-t))
282
+             (get-id-token (a-t) (cljwt:decode (assoc-cdr :id--token a-t) :fail-if-unsupported nil)))
283
+        (let* ((a-t (get-access-token *goog-endpoint-schema* (get-code params)))
284
+               (access-token (get-real-access-token a-t))
285
+               (id-token (get-id-token a-t))
286
+               (user-info (get-user-info *goog-endpoint-schema* access-token)))
287
+          (setf accesstoken access-token
288
+            app-user (funcall get-app-user-cb user-info id-token access-token)
289
+            idtoken id-token
290
+            userinfo user-info)
291
+          '(302 (:location "/"))))))
292
+
293
+  (def-callback-generator facebook-callback (get-app-user-cb) (params)
294
+    (auth-callback-skeleton params (:endpoint-schema *fbook-endpoint-schema*
295
+                                    :auth-session-vars (accesstoken userinfo idtoken app-user))
296
+      (flet ((get-id-token (a-t) (assoc-cdr :access--token a-t))) ; <-- access--token is not a mistake
297
+        (let* ((a-t (get-access-token *fbook-endpoint-schema* (get-code params)))
298
+               (id-token (get-id-token a-t))
299
+               (user-info (get-user-info *fbook-endpoint-schema* id-token)))
300
+          (setf accesstoken a-t
301
+            app-user (funcall get-app-user-cb user-info id-token a-t)
302
+            idtoken id-token
303
+            userinfo user-info)
304
+          '(302 (:location "/")))))))
295 305
 
296 306
 (defun userinfo-route (params)
297 307
   (declare (ignore params))
... ...
@@ -313,7 +323,7 @@
313 323
   (setf (route app "/userinfo.json" :method :get) #'userinfo-route
314 324
         (route app "/logout"  :method :get) #'logout-route
315 325
         (route app "/login/google" :method :get) #'google-login-entry
316
-        (route app "/login/facebook" :method :get) #'facebook-login-entry 
326
+        (route app "/login/facebook" :method :get) #'facebook-login-entry
317 327
         (route app "/oidc_callback/google" :method :get) (google-callback login-callback)
318 328
         (route app "/oidc_callback/facebook" :method :get) (facebook-callback login-callback))
319 329
   (lambda (app) (lambda (env) (funcall app env))))
... ...
@@ -328,6 +338,3 @@
328 338
            (setf (gethash :next-page ,session) nil)
329 339
            `(302 (:location ,next-page)))
330 340
          ,@body))))
331
-
332
-(export '(redirect-if-necessary def-route require-login))
333
-(export '(oauth2-login-middleware with-session))