Browse code
Generalizing for use in other apps
fiddlerwoaroof authored on 29/08/2015 23:22:08
Showing 3 changed files
Showing 3 changed files
... | ... |
@@ -33,41 +33,15 @@ |
33 | 33 |
|# |
34 | 34 |
|
35 | 35 |
(in-package :cl-oid-connect) |
36 |
+(declaim (optimize (debug 2))) |
|
36 | 37 |
; Should this be here? |
38 |
+(defparameter *oid* (make-instance 'ningle:<app>)) |
|
37 | 39 |
(setf drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*)) |
38 | 40 |
|
39 | 41 |
(sheeple:defproto =service-info= () |
40 | 42 |
((client-id nil :accessor t) |
41 | 43 |
(secret nil :accessor t))) |
42 | 44 |
|
43 |
-(defvar *FBOOK-INFO* (sheeple:clone =service-info=)) |
|
44 |
-(defun load-facebook-info (loadfrom) |
|
45 |
- (setf *FBOOK-INFO* |
|
46 |
- (with-open-file (fbook-info (truename loadfrom)) |
|
47 |
- (let* ((data (yason:parse fbook-info)) |
|
48 |
- (client-id (gethash "client-id" data)) |
|
49 |
- (secret (gethash "secret" data))) |
|
50 |
- (sheeple:defobject (=service-info=) |
|
51 |
- ((client-id client-id) |
|
52 |
- (secret secret))))))) |
|
53 |
- |
|
54 |
-(defvar *GOOG-INFO* (sheeple:clone =service-info=)) |
|
55 |
-(defun load-google-info (loadfrom) |
|
56 |
- (setf *GOOG-INFO* |
|
57 |
- (with-open-file (goog-info (truename loadfrom)) |
|
58 |
- (let* ((data (yason:parse goog-info)) |
|
59 |
- (client-id (gethash "client-id" data)) |
|
60 |
- (secret (gethash "secret" data))) |
|
61 |
- (sheeple:defobject (=service-info=) |
|
62 |
- ((client-id client-id) |
|
63 |
- (secret secret))))))) |
|
64 |
- |
|
65 |
-(load-facebook-info #p"facebook-secrets.json") |
|
66 |
-(load-google-info #p"google-secrets.json") |
|
67 |
- |
|
68 |
-(defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal)) |
|
69 |
-(defmacro assoc-cdr (key alist) `(cdr (assoc ,key ,alist))) |
|
70 |
- |
|
71 | 45 |
(sheeple:defproto =endpoint-schema= () |
72 | 46 |
((auth-endpoint nil :accessor t) |
73 | 47 |
(token-endpoint nil :accessor t) |
... | ... |
@@ -78,42 +52,11 @@ |
78 | 52 |
(sheeple:defreply get-user-info ((a =endpoint-schema=) (b sheeple:=string=))) |
79 | 53 |
(sheeple:defreply get-access-token ((a =endpoint-schema=) (b sheeple:=string=))) |
80 | 54 |
|
81 |
-(defun discover-endpoints (service-info discovery-doc-url &key (gat nil gat-p) (gui nil gui-p)) |
|
82 |
- (let ((discovery-document (cl-json:decode-json-from-string (drakma:http-request discovery-doc-url))) |
|
83 |
- (schema (sheeple:object :parents `(,=endpoint-schema= ,service-info)))) |
|
84 |
- |
|
85 |
- (setf (auth-endpoint schema) (assoc-cdr :authorization--endpoint discovery-document)) |
|
86 |
- (setf (token-endpoint schema) (assoc-cdr :token--endpoint discovery-document)) |
|
87 |
- |
|
88 |
- (if gui-p (sheeple:defreply get-user-info ((a schema)) (funcall gui a))) |
|
89 |
- (if gat-p (sheeple:defreply get-access-token ((a schema) (b sheeple:=string=)) |
|
90 |
- (funcall gat a b))) |
|
91 |
- |
|
92 |
- schema)) |
|
93 |
- |
|
94 |
-; This probably should eventually go? |
|
95 |
-(defvar *endpoint-schema* nil) |
|
96 |
-(defmacro with-endpoints (endpoint-schema &body body) |
|
97 |
- `(let* ((*endpoint-schema* ,endpoint-schema)) |
|
98 |
- ,@body)) |
|
99 |
- |
|
100 |
-(defun goog-get-access-token (endpoint-schema code) |
|
101 |
- (cl-json:decode-json-from-string |
|
102 |
- (drakma:http-request (token-endpoint endpoint-schema) |
|
103 |
- :method :post |
|
104 |
- :redirect nil |
|
105 |
- :parameters `(("code" . ,code) |
|
106 |
- ("client_id" . ,(client-id endpoint-schema)) |
|
107 |
- ("client_secret" . ,(secret endpoint-schema)) |
|
108 |
- ("redirect_uri" . ,(redirect-uri endpoint-schema)) |
|
109 |
- ("grant_type" . "authorization_code"))))) |
|
110 |
- |
|
55 |
+(defparameter *FBOOK-INFO* (sheeple:clone =service-info=)) |
|
56 |
+(defparameter *GOOG-INFO* (sheeple:clone =service-info=)) |
|
57 |
+(defparameter *endpoint-schema* nil) |
|
111 | 58 |
; goog is well behaved |
112 |
-(defvar *goog-endpoint-schema* |
|
113 |
- (discover-endpoints *GOOG-INFO* "https://accounts.google.com/.well-known/openid-configuration" |
|
114 |
- :gat #'goog-get-access-token)) |
|
115 |
-(setf (redirect-uri *goog-endpoint-schema*) "http://srv2.elangley.org:9090/oidc_callback/google") |
|
116 |
- |
|
59 |
+(defparameter *goog-endpoint-schema* (defobject (=endpoint-schema= *goog-info*))) |
|
117 | 60 |
|
118 | 61 |
; fbook needs personal attention |
119 | 62 |
(defproto *fbook-endpoint-schema* (=endpoint-schema= *fbook-info*) |
... | ... |
@@ -142,18 +85,25 @@ |
142 | 85 |
(drakma:http-request endpoint |
143 | 86 |
:parameters `(("access_token" . ,access-token)))))) |
144 | 87 |
|
145 |
-(defvar *fbook-mw* |
|
146 |
- (lambda (app) |
|
147 |
- (lambda (env) |
|
148 |
- (with-fbook-endpoints |
|
149 |
- (format t "~a" *client-id*) |
|
150 |
- (funcall app env))))) |
|
88 |
+(defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal)) |
|
89 |
+(defmacro assoc-cdr (key alist) `(cdr (assoc ,key ,alist))) |
|
151 | 90 |
|
152 |
-(defvar *goog-mw* |
|
153 |
- (lambda (app) |
|
154 |
- (lambda (env) |
|
155 |
- (with-goog-endpoints |
|
156 |
- (funcall app env))))) |
|
91 |
+(defun discover-endpoints (schema discovery-doc-url &key (gat nil gat-p) (gui nil gui-p)) |
|
92 |
+ (let ((discovery-document (cl-json:decode-json-from-string (drakma:http-request discovery-doc-url)))) |
|
93 |
+ |
|
94 |
+ (setf (auth-endpoint schema) (assoc-cdr :authorization--endpoint discovery-document)) |
|
95 |
+ (setf (token-endpoint schema) (assoc-cdr :token--endpoint discovery-document)) |
|
96 |
+ |
|
97 |
+ (if gui-p (sheeple:defreply get-user-info ((a schema)) (funcall gui a))) |
|
98 |
+ (if gat-p (sheeple:defreply get-access-token ((a schema) (b sheeple:=string=)) |
|
99 |
+ (funcall gat a b))) |
|
100 |
+ |
|
101 |
+ schema)) |
|
102 |
+ |
|
103 |
+; This probably should eventually go? |
|
104 |
+(defmacro with-endpoints (endpoint-schema &body body) |
|
105 |
+ `(let* ((*endpoint-schema* ,endpoint-schema)) |
|
106 |
+ ,@body)) |
|
157 | 107 |
|
158 | 108 |
(defun do-auth-request (endpoint-schema state) |
159 | 109 |
(format t "~%client-id: ~a~%" (auth-endpoint endpoint-schema)) |
... | ... |
@@ -172,9 +122,9 @@ |
172 | 122 |
(loop repeat len |
173 | 123 |
do (princ (random 36) stream))))) |
174 | 124 |
|
175 |
-(defvar *app* (make-instance 'ningle:<app>)) |
|
176 |
-(defmacro def-route (url args &body body) |
|
177 |
- `(setf (ningle:route *app* ,url) |
|
125 |
+ |
|
126 |
+(defmacro def-route ((url args &key (app *oid*)) &body body) |
|
127 |
+ `(setf (ningle:route ,app ,url) |
|
178 | 128 |
#'(lambda ,args |
179 | 129 |
(declare (ignorable ,@args)) |
180 | 130 |
,@body))) |
... | ... |
@@ -201,68 +151,131 @@ |
201 | 151 |
`(let ((,var (context :session))) |
202 | 152 |
,@body)) |
203 | 153 |
|
154 |
+(defun load-facebook-info (loadfrom) |
|
155 |
+ (with-open-file (fbook-info (truename loadfrom)) |
|
156 |
+ (let* ((data (yason:parse fbook-info)) |
|
157 |
+ (client-id (gethash "client-id" data)) |
|
158 |
+ (secret (gethash "secret" data))) |
|
159 |
+ (setf (client-id *FBOOK-INFO*) client-id) |
|
160 |
+ (setf (secret *FBOOK-INFO*) secret)))) |
|
204 | 161 |
|
205 |
-(def-route "/login/google" (params) |
|
206 |
- (with-session (session) |
|
207 |
- (let ((state (gen-state 36))) |
|
208 |
- (setf (gethash :state session) state) |
|
209 |
- (with-endpoints *goog-endpoint-schema* |
|
210 |
- (setf (gethash :endpoint-schema session) *goog-endpoint-schema*) |
|
211 |
- (multiple-value-bind (content rcode headers) (do-auth-request *goog-endpoint-schema* state) |
|
212 |
- (if (< rcode 400) |
|
213 |
- `(302 (:location ,(cdr (assoc :location headers)))) |
|
214 |
- content)))))) |
|
162 |
+(defun load-google-info (loadfrom) |
|
163 |
+ (with-open-file (goog-info (truename loadfrom)) |
|
164 |
+ (let* ((data (yason:parse goog-info)) |
|
165 |
+ (client-id (gethash "client-id" data)) |
|
166 |
+ (secret (gethash "secret" data))) |
|
167 |
+ (setf (client-id *GOOG-INFO*) client-id) |
|
168 |
+ (setf (secret *GOOG-INFO*) secret)))) |
|
215 | 169 |
|
170 |
+(defun goog-get-access-token (endpoint-schema code) |
|
171 |
+ (cl-json:decode-json-from-string |
|
172 |
+ (drakma:http-request (token-endpoint endpoint-schema) |
|
173 |
+ :method :post |
|
174 |
+ :redirect nil |
|
175 |
+ :parameters `(("code" . ,code) |
|
176 |
+ ("client_id" . ,(client-id endpoint-schema)) |
|
177 |
+ ("client_secret" . ,(secret endpoint-schema)) |
|
178 |
+ ("redirect_uri" . ,(redirect-uri endpoint-schema)) |
|
179 |
+ ("grant_type" . "authorization_code"))))) |
|
216 | 180 |
|
217 |
-(def-route "/login/facebook" (params) |
|
218 |
- (with-session (session) |
|
219 |
- (let ((state (gen-state 36))) |
|
220 |
- (setf (gethash :state session) state) |
|
221 |
- (with-endpoints *fbook-endpoint-schema* |
|
222 |
- (setf (gethash :endpoint-schema session) *fbook-endpoint-schema*) |
|
223 |
- (multiple-value-bind (content rcode headers uri) (do-auth-request *fbook-endpoint-schema* state) |
|
224 |
- (if (< rcode 400) |
|
225 |
- `(302 (:location ,(format nil "~a" uri))) |
|
226 |
- content)))))) |
|
227 |
- |
|
228 |
-(def-route "/oidc_callback/google" (params) |
|
229 |
- (let ((received-state (cdr (string-assoc "state" params))) |
|
230 |
- (code (cdr (string-assoc "code" params)))) |
|
231 |
- (check-state received-state |
|
232 |
- (with-session (session) |
|
233 |
- (with-endpoints *goog-endpoint-schema* |
|
234 |
- (let* ((a-t (get-access-token *goog-endpoint-schema* code)) |
|
235 |
- (id-token (assoc-cdr :id--token a-t)) |
|
236 |
- (decoded (cljwt:decode id-token :fail-if-unsupported nil))) |
|
237 |
- (setf (gethash :userinfo session) decoded) |
|
238 |
- '(302 (:location "/"))))) |
|
239 |
- '(403 '() "Out, vile imposter!")))) |
|
240 |
- |
|
241 |
- |
|
242 |
-(def-route "/oidc_callback/facebook" (params) |
|
243 |
- (let ((received-state (cdr (string-assoc "state" params))) |
|
244 |
- (code (cdr (string-assoc "code" params)))) |
|
245 |
- (with-endpoints *fbook-endpoint-schema* |
|
246 |
- (check-state received-state |
|
247 |
- (with-session (session) |
|
248 |
- (let* ((a-t (get-access-token *fbook-endpoint-schema* code)) |
|
249 |
- (id-token (assoc-cdr :access--token a-t))) |
|
250 |
- (setf (gethash :userinfo session) (get-user-info *fbook-endpoint-schema* id-token)) |
|
251 |
- '(302 (:location "/")))) |
|
252 |
- '(403 '() "Out, vile imposter!"))))) |
|
253 |
- |
|
254 |
-(def-route "/userinfo.json" (params) |
|
255 |
- (with-session (session) |
|
256 |
- (require-login |
|
257 |
- (with-endpoints (gethash :endpoint-schema session) |
|
258 |
- (cl-json:encode-json-to-string (gethash :userinfo session)))))) |
|
181 |
+(defun load-goog-endpoint-schema () |
|
182 |
+ (discover-endpoints *goog-endpoint-schema* |
|
183 |
+ "https://accounts.google.com/.well-known/openid-configuration" |
|
184 |
+ :gat #'goog-get-access-token) |
|
185 |
+ (setf (redirect-uri *goog-endpoint-schema*) "http://srv2.elangley.org:9090/oidc_callback/google")) |
|
259 | 186 |
|
260 |
-(def-route "/logout" (params) |
|
261 |
- (with-session (session) |
|
262 |
- (setf (gethash :userinfo session) nil) |
|
263 |
- '(302 (:location "/")))) |
|
264 | 187 |
|
265 |
-(def-route "/login" (params) |
|
188 |
+(defun oauth2-login-middleware (&key google-info facebook-info) |
|
189 |
+ (lambda (app) |
|
190 |
+ (load-facebook-info facebook-info) |
|
191 |
+ (load-goog-endpoint-schema) |
|
192 |
+ (load-google-info google-info) |
|
193 |
+ |
|
194 |
+ (def-route ("/login/google" (params) :app app) |
|
195 |
+ (with-session (session) |
|
196 |
+ (let ((state (gen-state 36))) |
|
197 |
+ (setf (gethash :state session) state) |
|
198 |
+ (with-endpoints *goog-endpoint-schema* |
|
199 |
+ (setf (gethash :endpoint-schema session) *goog-endpoint-schema*) |
|
200 |
+ (multiple-value-bind (content rcode headers) (do-auth-request *goog-endpoint-schema* state) |
|
201 |
+ (if (< rcode 400) |
|
202 |
+ `(302 (:location ,(cdr (assoc :location headers)))) |
|
203 |
+ content)))))) |
|
204 |
+ |
|
205 |
+ |
|
206 |
+ (def-route ("/login/facebook" (params) :app app) |
|
207 |
+ (with-session (session) |
|
208 |
+ (let ((state (gen-state 36))) |
|
209 |
+ (setf (gethash :state session) state) |
|
210 |
+ (with-endpoints *fbook-endpoint-schema* |
|
211 |
+ (setf (gethash :endpoint-schema session) *fbook-endpoint-schema*) |
|
212 |
+ (multiple-value-bind (content rcode headers uri) (do-auth-request *fbook-endpoint-schema* state) |
|
213 |
+ (declare (ignore headers)) |
|
214 |
+ (if (< rcode 400) |
|
215 |
+ `(302 (:location ,(format nil "~a" uri))) |
|
216 |
+ content)))))) |
|
217 |
+ |
|
218 |
+ (def-route ("/oidc_callback/google" (params) :app app) |
|
219 |
+ (let ((received-state (cdr (string-assoc "state" params))) |
|
220 |
+ (code (cdr (string-assoc "code" params)))) |
|
221 |
+ (check-state received-state |
|
222 |
+ (with-session (session) |
|
223 |
+ (with-endpoints *goog-endpoint-schema* |
|
224 |
+ (let* ((a-t (get-access-token *goog-endpoint-schema* code)) |
|
225 |
+ (id-token (assoc-cdr :id--token a-t)) |
|
226 |
+ (decoded (cljwt:decode id-token :fail-if-unsupported nil))) |
|
227 |
+ (setf (gethash :userinfo session) decoded) |
|
228 |
+ '(302 (:location "/"))))) |
|
229 |
+ '(403 '() "Out, vile imposter!")))) |
|
230 |
+ |
|
231 |
+ |
|
232 |
+ (def-route ("/oidc_callback/facebook" (params) :app app) |
|
233 |
+ (let ((received-state (cdr (string-assoc "state" params))) |
|
234 |
+ (code (cdr (string-assoc "code" params)))) |
|
235 |
+ (with-endpoints *fbook-endpoint-schema* |
|
236 |
+ (check-state received-state |
|
237 |
+ (with-session (session) |
|
238 |
+ (let* ((a-t (get-access-token *fbook-endpoint-schema* code)) |
|
239 |
+ (id-token (assoc-cdr :access--token a-t))) |
|
240 |
+ (setf (gethash :userinfo session) (get-user-info *fbook-endpoint-schema* id-token)) |
|
241 |
+ '(302 (:location "/")))) |
|
242 |
+ '(403 '() "Out, vile imposter!"))))) |
|
243 |
+ |
|
244 |
+ (def-route ("/userinfo.json" (params) :app app) |
|
245 |
+ (with-session (session) |
|
246 |
+ (require-login |
|
247 |
+ (with-endpoints (gethash :endpoint-schema session) |
|
248 |
+ (cl-json:encode-json-to-string (gethash :userinfo session)))))) |
|
249 |
+ |
|
250 |
+ (def-route ("/logout" (params) :app app) |
|
251 |
+ (with-session (session) |
|
252 |
+ (setf (gethash :userinfo session) nil) |
|
253 |
+ '(302 (:location "/")))) |
|
254 |
+ |
|
255 |
+ app)) |
|
256 |
+ |
|
257 |
+ |
|
258 |
+(defmacro redirect-if-necessary (sessionvar &body body) |
|
259 |
+ (with-gensyms (session) |
|
260 |
+ `(let* ((,session ,sessionvar) |
|
261 |
+ (next-page (gethash :next-page ,session))) |
|
262 |
+ (if (and (not (null next-page)) |
|
263 |
+ (not (string= next-page (lack.request:request-path-info *request*)))) |
|
264 |
+ (progn |
|
265 |
+ (setf (gethash :next-page ,session) nil) |
|
266 |
+ `(302 (:location ,next-page))) |
|
267 |
+ ,@body)))) |
|
268 |
+ |
|
269 |
+(export '(redirect-if-necessary def-route require-login)) |
|
270 |
+(export '(oauth2-login-middleware with-session)) |
|
271 |
+ |
|
272 |
+(in-package :cl-user) |
|
273 |
+(import '(cl-oid-connect:redirect-if-necessary cl-oid-connect:def-route cl-oid-connect:require-login)) |
|
274 |
+(import '(cl-oid-connect:oauth2-login-middleware cl-oid-connect:with-session)) |
|
275 |
+ |
|
276 |
+(defparameter *app* (make-instance 'ningle:<app>)) |
|
277 |
+ |
|
278 |
+(def-route ("/login" (params) :app *app*) |
|
266 | 279 |
(cl-who:with-html-output-to-string (s) |
267 | 280 |
(:html |
268 | 281 |
(:head |
... | ... |
@@ -271,16 +284,23 @@ |
271 | 284 |
(:div (:a :href "/login/facebook" "Facebook")) |
272 | 285 |
(:div (:a :href "/login/google" "Google")))))) |
273 | 286 |
|
274 |
-(def-route "/" (params) |
|
287 |
+(def-route ("/" (params) :app *app*) |
|
275 | 288 |
(with-session (session) |
276 |
- (if (not (null (gethash :next-page session))) |
|
277 |
- `(302 (:location ,(gethash :next-page session))) |
|
289 |
+ (redirect-if-necessary session |
|
278 | 290 |
(require-login |
279 | 291 |
(anaphora:sunless (gethash :counter session) (setf anaphora:it 0)) |
292 |
+ (incf (gethash :counter session)) |
|
280 | 293 |
(format nil "~Ath visit<br/>~a<br/><br/>~S<br/>" |
281 | 294 |
(gethash :counter session) |
282 | 295 |
(alexandria:hash-table-alist session) |
283 |
- (alexandria:hash-table-alist (context :session))))))) |
|
284 |
- |
|
285 |
-(setf *handler* (clack:clackup (lack.builder:builder :session *app*) :port 9090)) |
|
296 |
+ (alexandria:hash-table-alist (ningle:context :session))))))) |
|
297 |
+ |
|
298 |
+(setf *handler* (clack:clackup (lack.builder:builder |
|
299 |
+ :backtrace |
|
300 |
+ :session |
|
301 |
+ (funcall |
|
302 |
+ (oauth2-login-middleware |
|
303 |
+ :facebook-info (truename "facebook-secrets.json") |
|
304 |
+ :google-info (truename "google-secrets.json")) |
|
305 |
+ *app*)) :port 9090)) |
|
286 | 306 |
|
... | ... |
@@ -1,14 +1,14 @@ |
1 | 1 |
;;;; package.lisp |
2 |
-(ql:quickload :ningle) |
|
3 |
-(ql:quickload :clack) |
|
4 |
-(ql:quickload :drakma) |
|
5 |
-(ql:quickload :cljwt) |
|
6 |
-(ql:quickload :cl-json) |
|
7 |
-(ql:quickload :anaphora) |
|
8 |
-(ql:quickload :alexandria) |
|
9 |
-(ql:quickload :lack-middleware-session) |
|
10 |
-(ql:quickload :cl-who) |
|
11 |
-(ql:quickload :sheeple) |
|
2 |
+;(ql:quickload :ningle) |
|
3 |
+;(ql:quickload :clack) |
|
4 |
+;(ql:quickload :drakma) |
|
5 |
+;(ql:quickload :cljwt) |
|
6 |
+;(ql:quickload :cl-json) |
|
7 |
+;(ql:quickload :anaphora) |
|
8 |
+;(ql:quickload :alexandria) |
|
9 |
+;(ql:quickload :lack-middleware-session) |
|
10 |
+;(ql:quickload :cl-who) |
|
11 |
+;(ql:quickload :sheeple) |
|
12 | 12 |
|
13 | 13 |
(defpackage :cl-oid-connect |
14 | 14 |
(:use #:cl #:drakma #:ningle #:clack #:cljwt #:anaphora #:alexandria #:sheeple)) |