Racket Stories and Web Tutorial
The following describes how Racket Stories and the servers in the Web Tutorial handles logins.
There are of course other ways of approaching logins and sessions so comments are welcome.
The web server running Racket Stories is stateless in the sense that all state is in the database.
The earlier versions of Racket Stories are available in the "web-tutorial" repo and
are meant to be easy to try out. The servers in the Web Tutorial gradually adds more features.
The first version with user authentication is listit2
:
Passwords
The first thing to consider is how passwords are stored.
The principles used are as follows.
-
Don't store passwords in plain text
(It's a problem if an attacker gets access to the database)
Store a key derived from the password.
-
Don't store a simple hash of the password.
(An attacker might have rainbow tables)
Conclusion: Use a salt (random string, non-secret)
together with the password to derive a key.
-
Use a standard KDF (Key Derivation function)
Conclusion: Use the crypto
package.
-
Use the Argon2 algorithm, but fallback to PBKDF2 when not available.
Side note:
Salts aren't secret. Their only purpose is to make a rainbow table attack impossible.
See "authentication.rkt" for details on how keys are computed from password and salt.
Authentication
The user table in the database stores the key.
The login in the model is simple:
- get the user
- check that the password corresponds to the stored key
(define (authenticate-user username password)
(match (get-user/username username)
[#f (authentication-error "username not in db")]
[u (if (verify-password (user-key u))
#t
(authentication-error "wrong password"))]))
The actual function that verifies the password is:
(define (verify-password password key)
(pwhash-verify kdf-impl password key))
It uses pwhash-verify
from the crypto
package.
Login Sessions
Let's say the user successfully logged in and sends a request for a new page.
How do we know the user is logged in?
We need to give the user some way to prove that he is logged in.
We do this by storing the login information in the users browser in the form of a cookie.
Since the information is stored at the client - we need to prevent any tampering.
The web-server provides us with make-id-cookie
which works by storing a digest of the data next to the data. If the digest and data doesn't match, someone tampered with the data - and we can ignore it.
The digest uses a secret salt, so in "control.rkt" we have this:
(define-runtime-path cookie-salt.bin "cookie-salt.bin")
(def cookie-salt (make-secret-salt/file cookie-salt.bin))
(define (make-logged-in-cookie)
(make-id-cookie "login-status" "in" #:key cookie-salt #:http-only? #t ))
(define (get-cookie-value req name)
(request-id-cookie req #:name name #:key cookie-salt))
(define (get-login-status req)
(match (get-cookie-value req "login-status")
["in" #t]
[_ #f]))
The dispatch function checks the login status and stores it in a paramer before the dispatch happens:
(define (dispatch-on-action req)
(current-request req)
(def login-status (get-login-status req))
(def username (and login-status (get-cookie-value req "username")))
(def user (and username (get-user #:username username)))
(parameterize ([current-login-status (and user login-status)]
[current-user (and login-status user)])
(match (get-binding #"action")
[#"updown" (do-updown)] ; a voting arrow was clicked
[#"submitnew" (do-submit-new)] ; the "submit new" link on the front page was clicked
[#"submit" (do-submit)] ; new entry sent from the new entry page
[#"about" (do-about)] ; show the about page
[#"login" (do-login-page)] ; show the login page
[#"submit-login" (do-submit-login)] ; check username and password
[#"logout" (do-logout)] ; logout, then show front page
[#"submit-create-account" ; create new user
(do-submit-create-account)]
[_ (do-front-page)]))) ; show front page