Skip to content

Commit 259e769

Browse files
committed
users and passwords, easy-routes decorators
1 parent 13ecf9b commit 259e769

File tree

2 files changed

+121
-25
lines changed

2 files changed

+121
-25
lines changed

content/building-blocks/database.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ title = "Connecting to a database"
33
weight = 120
44
+++
55

6-
<!-- https://www.reddit.com/r/Common_Lisp/comments/1f7bfql/simple_session_management_with_hunchentoot/ -->
76

87
Let's study different use cases:
98

content/building-blocks/users-and-passwords.md

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,143 @@ title = "Users and passwords"
33
weight = 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

11107
In Caveman, `*session*` is a hash table that represents the session's
12108
data. 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

24120
We 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,
42139
and we render the login page. When all is well, we execute the macro's
43140
body. 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

58155
and 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

76173
You might also want to look at
77174
[hermetic](https://github.com/eudoxia0/hermetic), a simple
78175
authentication system for Clack-based applications.
79176

80-
#### Manually (with Ironclad)
177+
### Manually (with Ironclad)
81178

82179
In 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.
87184
The following snippet creates the password hash that should be stored in your
88185
database. 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).
97194
It 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
102199
nil in all other cases even if an error occurs. Adapt it to your
103200
application.
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

117214
And the following is an example on how to set the password on the
118215
database. Note that we use `(password-hash password)` to save the
119216
password. The rest is specific to the web framework and to the DB
120217
library.
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

Comments
 (0)