Browse code
Initial commit
This currently works with Google and runs a (very simple) site to demonstrate its
functionality.
Showing 8 changed files
- .gitignore
- COPYING
- README.md
- TODO.md
- cl-oid-connect.asd
- cl-oid-connect.lisp
- google-secrets.json.dist
- package.lisp
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 | 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 |
+ |