git.fiddlerwoaroof.com
Browse code

Initial commit

This currently works with Google and runs a (very simple) site to demonstrate its
functionality.

fiddlerwoaroof authored on 22/08/2015 16:29:04
Showing 8 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
1
+google-secrets.json
2
+.*.sw?
0 3
new file mode 100644
... ...
@@ -0,0 +1,29 @@
1
+Copyright (c) 2015 Edward Langley
2
+All rights reserved.
3
+
4
+Redistribution and use in source and binary forms, with or without
5
+modification, are permitted provided that the following conditions
6
+are met:
7
+
8
+Redistributions of source code must retain the above copyright notice,
9
+this list of conditions and the following disclaimer.
10
+
11
+Redistributions in binary form must reproduce the above copyright
12
+notice, this list of conditions and the following disclaimer in the
13
+documentation and/or other materials provided with the distribution.
14
+
15
+Neither the name of the project's author nor the names of its
16
+contributors may be used to endorse or promote products derived from
17
+this software without specific prior written permission.
18
+
19
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 30
new file mode 100644
... ...
@@ -0,0 +1,7 @@
1
+This project implements the OpenId Connect API. In its current state, it can
2
+authenticate a user against Google and display the informaiton Google sends
3
+back. The only tricky requirement it has is cljwt, a library for parsing JSON
4
+Web Tokens, which is not in quicklisp but can be gotten from
5
+<https://github.com/fiddlerwoaroof/cljwt>. (This is my fork, I've made a couple
6
+changes since the original library didn't support the signature algorithm Google
7
+uses).
1 9
new file mode 100644
... ...
@@ -0,0 +1,11 @@
1
+;;;; cl-oid-connect.asd
2
+
3
+(asdf:defsystem :cl-oid-connect
4
+  :description "Describe cl-oid-connect here"
5
+  :author "Your Name <your.name@example.com>"
6
+  :license "Specify license here"
7
+  :depends-on (#:drakma)
8
+  :serial t
9
+  :components ((:file "package")
10
+               (:file "cl-oid-connect")))
11
+
0 12
new file mode 100644
... ...
@@ -0,0 +1,147 @@
1
+;;;; cl-oid-connect.lisp
2
+#|
3
+ |Copyright (c) 2015 Edward Langley
4
+ |All rights reserved.
5
+ |
6
+ |Redistribution and use in source and binary forms, with or without
7
+ |modification, are permitted provided that the following conditions
8
+ |are met:
9
+ |
10
+ |Redistributions of source code must retain the above copyright notice,
11
+ |this list of conditions and the following disclaimer.
12
+ |
13
+ |Redistributions in binary form must reproduce the above copyright
14
+ |notice, this list of conditions and the following disclaimer in the
15
+ |documentation and/or other materials provided with the distribution.
16
+ |
17
+ |Neither the name of the project's author nor the names of its
18
+ |contributors may be used to endorse or promote products derived from
19
+ |this software without specific prior written permission.
20
+ |
21
+ |THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
+ |"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
+ |LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24
+ |FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25
+ |HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
+ |SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES  INCLUDING, BUT NOT LIMITED
27
+ |TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
28
+ |PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
29
+ |LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30
+ |NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31
+ |SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ |
33
+ |#
34
+
35
+(in-package :cl-oid-connect)
36
+(setq drakma:*text-content-types* (cons '("application" . "json") drakma:*text-content-types*))
37
+
38
+(with-open-file (goog-info #P"google-secrets.json")
39
+  (let* ((data (yason:parse goog-info))
40
+         (client-id (gethash "client-id" data))
41
+         (secret (gethash "secret" data)))
42
+    (defconstant *GOOG-CLIENT-ID* client-id)
43
+    (defconstant *GOOG-CLIENT-SECRET* secret)))
44
+
45
+;;; "cl-oid-connect" goes here. Hacks and glory await!
46
+(defvar *app* (make-instance 'ningle:<app>))
47
+(defvar *state* nil)
48
+
49
+;; These tokens specify the auth endpoint. These are autodiscovered, if the relevant
50
+;; functions are wrapped with the "with-goog-endpoints" macro.
51
+(defvar *auth-endpoint* nil)
52
+(defvar *token-endpoint* nil)
53
+
54
+(defmacro string-assoc (key alist) `(assoc ,key ,alist :test #'equal))
55
+(defmacro assoc-cdr (key alist) `(cdr (assoc ,key ,alist)))
56
+
57
+(defmacro with-goog-endpoints (&body body)
58
+  (alexandria:with-gensyms (discovery-document)
59
+    `(let* ((,discovery-document
60
+              (cl-json:decode-json-from-string
61
+                (drakma:http-request "https://accounts.google.com/.well-known/openid-configuration")))
62
+            (*auth-endpoint* (assoc-cdr :authorization--endpoint ,discovery-document))
63
+            (*token-endpoint* (assoc-cdr :token--endpoint ,discovery-document)))
64
+       ,@body)))     
65
+
66
+(defvar *goog-mw*
67
+  (lambda (app)
68
+    (lambda (env)
69
+      (with-goog-endpoints
70
+        (funcall app env)))))
71
+
72
+(defun get-access-token (code)
73
+  (cl-json:decode-json-from-string
74
+    (drakma:http-request *token-endpoint*
75
+                         :method :post
76
+                         :redirect nil
77
+                         :parameters `(("code" . ,code)
78
+                                       ("client_id" . ,*GOOG-CLIENT-ID*)
79
+                                       ("client_secret" . ,*GOOG-CLIENT-SECRET*)
80
+                                       ("redirect_uri" . "http://srv2.elangley.org:9090/oidc_callback")
81
+                                       ("grant_type" . "authorization_code")))))
82
+
83
+(defun do-auth-request (state)
84
+  (drakma:http-request *auth-endpoint*
85
+                       :redirect nil
86
+                       :parameters `(("client_id" . ,*GOOG-CLIENT-ID*)
87
+                                     ("response_type" . "code")
88
+                                     ("scope" . "openid email")
89
+                                     ("redirect_uri" . "http://srv2.elangley.org:9090/oidc_callback")
90
+                                     ("state" . ,state))))
91
+
92
+(defun gen-state (len)
93
+  (with-output-to-string (stream)
94
+    (let ((*print-base* 36))
95
+      (loop repeat len
96
+            do (princ (random 36) stream)))))
97
+
98
+(defmacro def-route (url args &body body)
99
+  `(setf (ningle:route *app* ,url)
100
+         #'(lambda ,args
101
+             ,@body)))
102
+
103
+(defmacro check-state (received-state then else)
104
+  (alexandria:with-gensyms (saved-state)
105
+    `(let ((,saved-state (gethash :state *session*)))
106
+       (if (equal ,saved-state ,received-state)
107
+         ,then
108
+         ,else))))
109
+
110
+(defmacro require-login (&body body)
111
+  `(if (not (eql nil (gethash :userinfo *session*)))
112
+     (progn
113
+       ,@body)
114
+     '(302 (:location "/login"))))
115
+
116
+(def-route "/login" (params)
117
+  (declare (ignore params))
118
+  (let ((state (gen-state 36)))
119
+    (setf (gethash :state *session*) state)
120
+    (multiple-value-bind (content rcode headers) (do-auth-request state)
121
+      (if (< rcode 400)
122
+        `(302 (:location ,(cdr (assoc :location headers))))
123
+        content))))
124
+
125
+(def-route "/oidc_callback" (params)
126
+  (let ((received-state (cdr (string-assoc "state" params)))
127
+        (code (cdr (string-assoc "code" params))))
128
+    (check-state received-state
129
+                 (let* ((a-t (get-access-token code)) (id-token (assoc-cdr :id--token a-t))
130
+                        (decoded (cljwt:decode id-token :fail-if-unsupported nil)))
131
+                   (setf (gethash :userinfo *session*) decoded)
132
+                   '(302 (:location "/")))
133
+                 '(403 '() "Out, vile imposter!"))))
134
+
135
+(def-route "/" (params)
136
+  (require-login 
137
+    (anaphora:sunless (gethash :counter *session*) (setf anaphora:it 0))
138
+    (format nil "~Ath visit<br/>~a<br/>~S"
139
+            (incf (gethash :counter *session*))
140
+            *state*
141
+            (alexandria:hash-table-alist *session*))))
142
+
143
+
144
+
145
+(setf *handler* (clack:clackup (lack.builder:builder :session *goog-mw* *app*)
146
+                               :port 9090))
147
+
0 148
new file mode 100644
... ...
@@ -0,0 +1,5 @@
1
+{
2
+  "client-id": "XXX",
3
+  "secret": "XXX"
4
+}
5
+
0 6
new file mode 100644
... ...
@@ -0,0 +1,13 @@
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
+
11
+(defpackage :cl-oid-connect
12
+  (:use #:cl #:drakma #:ningle #:clack #:cljwt #:anaphora #:alexandria))
13
+