@@ -3,46 +3,143 @@ title = "Users and passwords"
33weight = 130
44+++
55
6- ### Checking a user is logged-in
6+ We don't know of a Common Lisp framework that will create users and
7+ roles for you and protect your routes. You'll have to write some Lisp.
78
8- A framework will provide a way to work with sessions. We'll create a
9- little macro to wrap our routes to check if the user is logged in.
9+ {{% notice info %}}
10+
11+ Stay tuned! We are on to something.
12+
13+ {{% /notice %}}
14+
15+ ## Creating users
16+
17+ If you use a database, you'll have to create at least to save users. A
18+ ` user ` table typically defines:
19+ - a unique ID (the primary key)
20+ - a name (varchar)
21+ - an email (varchar)
22+ - a password (encrypted)
23+ - optionally, a key to the table listing roles.
24+
25+
26+ ## Checking a user is logged-in
27+
28+ <!-- https://www.reddit.com/r/Common_Lisp/comments/1f7bfql/simple_session_management_with_hunchentoot/ -->
29+
30+ A framework provides a way to work with sessions.
31+
32+ Our web app defines routes. Routes are, in the end, only functions.
33+
34+ We must find a way to check for users before the route function. If
35+ our check is successful (the user registered in the current session)
36+ is logged-in, we call the next function (the route body). If our check
37+ is falsy, we don't execute the route but we redirect to the login page
38+ instead. Simple right?
39+
40+
41+ ### Hunchentoot and easy-routes
42+
43+ ` easy-routes ` provides us with an easy way to call any function before
44+ the route body. Following the naming of a popular language, they are
45+ called "decorators".
46+
47+ Remember the shape of our routes:
48+
49+ ``` lisp
50+ (easy-routes:defroute root ("/") ()
51+ "hello app")
52+ ```
53+
54+ We add a list of decorators after the ` "/" ` part, like this:
55+
56+ ``` lisp
57+ (defroute my-protected-route ("/foo" :method :get
58+ :decorators ((@json)))
59+ ()
60+ "hello decorated route")
61+ ```
62+
63+ but what is ` @json ` ? It's a function:
64+
65+ ``` lisp
66+ (defun @json (next)
67+ (setf (hunchentoot:content-type*) "application/json")
68+ (funcall next))
69+ ```
70+
71+ You can ignore the ` @ ` sign, it doesn't mean anything in Common Lisp
72+ (but as it's part of the function name it can help you reference all
73+ your route decorators).
74+
75+ Route "decorators" must accept at least one argument: ` next ` , the
76+ function that will be called next (so, at one point, our route body)
77+ * if we want to* . Look at ` (funcall next) ` : our decorator correctly
78+ calls it.
79+
80+ So what it is function doing? Keep it mind that it is called in the
81+ context of a web request. So we can call ` hunchentoot:content-type* `
82+ (note the ` * ` , this function is applied on the current web
83+ request). We are setting this request's content-type to
84+ ` application/json ` .
85+
86+ Yes, you can copy-paste the ` setf ` line directly into your function.
87+
88+ Here's another "decorator" (straight from easy-routes' documentation):
89+
90+ ``` lisp
91+ (defun @auth (next)
92+ (let ((*user* (hunchentoot:session-value 'user)))
93+ (if (not *user*)
94+ (hunchentoot:redirect "/login")
95+ (funcall next))))
96+ ```
97+
98+ Now that's interesting. It's doing this:
99+ - it gets a value from the current web session. This can be any Lisp object.
100+ - it binds it to a global variable (we suppose the application re-uses it later) (global variables are thread-local)
101+ - if a user was registered in the session, we call the ` next ` method to run other decorators and the route body
102+ - otherwise, we redirect to the login page.
103+
104+
105+ ### Caveman
10106
11107In Caveman, ` *session* ` is a hash table that represents the session's
12108data. Here are our login and logout functions:
13109
14- ~~~ lisp
110+ ``` lisp
15111(defun login (user)
16112 "Log the user into the session"
17113 (setf (gethash :user *session*) user))
18114
19115(defun logout ()
20116 "Log the user out of the session."
21117 (setf (gethash :user *session*) nil))
22- ~~~
118+ ```
23119
24120We define a simple predicate:
25121
26- ~~~ lisp
122+ ``` lisp
27123(defun logged-in-p ()
28124 (gethash :user cm:*session*))
29- ~~~
125+ ```
30126
31- and we define our ` with-logged-in ` macro:
127+ We don't know a mechanism as easy-routes' "decorators" but we define a
128+ ` with-logged-in ` macro:
32129
33- ~~~ lisp
130+ ``` lisp
34131(defmacro with-logged-in (&body body)
35132 `(if (logged-in-p)
36133 (progn ,@body)
37134 (render #p"login.html"
38135 '(:message "Please log-in to access this page."))))
39- ~~~
136+ ```
40137
41- If the user isn't logged in, there will nothing in the session store,
138+ If the user isn't logged in, there will be nothing stored in the session store,
42139and we render the login page. When all is well, we execute the macro's
43140body. We use it like this:
44141
45- ~~~ lisp
142+ ``` lisp
46143(defroute "/account/logout" ()
47144 "Show the log-out page, only if the user is logged in."
48145 (with-logged-in
@@ -53,31 +150,31 @@ body. We use it like this:
53150 (with-logged-in
54151 (render #p"review.html"
55152 (list :review (get-review (gethash :user *session*))))))
56- ~~~
153+ ```
57154
58155and so on.
59156
60157
61- ### Encrypting passwords
158+ ## Encrypting passwords
62159
63- #### With cl-pass
160+ ### With cl-pass
64161
65162[ cl-pass] ( https://github.com/eudoxia0/cl-pass ) is a password hashing and verification library. It is as simple to use as this:
66163
67- ~~~ lisp
164+ ``` lisp
68165(cl-pass:hash "test")
69166;; "PBKDF2$sha256:20000$5cf6ee792cdf05e1ba2b6325c41a5f10$19c7f2ccb3880716bf7cdf999b3ed99e07c7a8140bab37af2afdc28d8806e854"
70167(cl-pass:check-password "test" *)
71168;; t
72169(cl-pass:check-password "nope" **)
73170;; nil
74- ~~~
171+ ```
75172
76173You might also want to look at
77174[ hermetic] ( https://github.com/eudoxia0/hermetic ) , a simple
78175authentication system for Clack-based applications.
79176
80- #### Manually (with Ironclad)
177+ ### Manually (with Ironclad)
81178
82179In this recipe we do the encryption and verification ourselves. We use the de-facto standard
83180[ Ironclad] ( https://github.com/froydnj/ironclad ) cryptographic toolkit
@@ -87,11 +184,11 @@ encoding/decoding library.
87184The following snippet creates the password hash that should be stored in your
88185database. Note that Ironclad expects a byte-vector, not a string.
89186
90- ~~~ lisp
187+ ``` lisp
91188(defun password-hash (password)
92189 (ironclad:pbkdf2-hash-password-to-combined-string
93190 (babel:string-to-octets password)))
94- ~~~
191+ ```
95192
96193` pbkdf2 ` is defined in [ RFC2898] ( https://tools.ietf.org/html/rfc2898 ) .
97194It uses a pseudorandom function to derive a secure encryption key
@@ -102,7 +199,7 @@ entered password. It returns the user-id if active and verified and
102199nil in all other cases even if an error occurs. Adapt it to your
103200application.
104201
105- ~~~ lisp
202+ ``` lisp
106203(defun check-user-password (user password)
107204 (handler-case
108205 (let* ((data (my-get-user-data user))
@@ -112,14 +209,14 @@ application.
112209 hash))
113210 (my-get-user-id data)))
114211 (condition () nil)))
115- ~~~
212+ ```
116213
117214And the following is an example on how to set the password on the
118215database. Note that we use ` (password-hash password) ` to save the
119216password. The rest is specific to the web framework and to the DB
120217library.
121218
122- ~~~ lisp
219+ ``` lisp
123220(defun set-password (user password)
124221 (with-connection (db)
125222 (execute
@@ -130,6 +227,6 @@ library.
130227 :id_user
131228 :email)
132229 user))))))
133- ~~~
230+ ```
134231
135232* Credit: ` /u/arvid ` on [ /r/learnlisp] ( https://www.reddit.com/r/learnlisp/comments/begcf9/can_someone_give_me_an_eli5_on_hiw_to_encrypt_and/ ) * .
0 commit comments