- Rust 100%
| src | bunch of changes (save after delete and set expiration for cookie) | |
| .gitignore | cleaning up stuff | |
| Cargo.lock | add cookie hardening and configuration using dotenv | |
| Cargo.toml | add cookie hardening and configuration using dotenv | |
| README.md | add cookie hardening and configuration using dotenv | |
Splonk: A very simple authentication system
Splonk is a very simple but versatile subrequest-based authentication middleware.
Features
- Fairly robust authentication (similar to Mullvad VPN's system)
- Role-based authorization
- Very light (<300Kb)
- Very simple (<300 lines of Rust)
- Very versatile (uses subrequest system which means most of the auth-related config can be done directly in the reverse proxy configuration)
- Doesn't require JavaScript to interact with it from a browser
- Is one single binary with no external dependency (not even a database)
- It's fast and memory-safe
- Registration done using invites
Roadmap
- Authorize endpoint
- Login and register
- Invite management
- User management
- Logout (and deletion of session, or setting sessions as solely in-memory?)
- Return user attributes and id in response headers
- Proper configuration (for cookie hardening and configuration of session durations)
- Let a route accept multiple roles
- Removing many url params to use GET parmas instead (to be compatible with forms again)
Setting up
# Installing and running this project
cargo install --git https://codeberg.org/SnowCode/splonk
splonk
# Creating a new admin invite with all the roles (use the same command to create invite codes for other roles too)
curl -d roles=admin&roles=user http://localhost:3000/auth/admin/invites
# Retrieve the invite code from the response
# Create a new admin account using the invite
curl -d invite=YOUR_INVITE_CODE_HERE http://localhost:3000/auth/user/register
# Retrieve the user token from the response (used to login as admin later)
# Delete the invite
curl -X DELETE http://localhost:3000/auth/admin/invites/YOUR_INVITE_CODE_HERE
Now we can also configure a login and register page using HTML (of course you could also do the requests in Javascript if you want to):
<h2>Login</h2>
<form action="/auth/user/login" method="post">
<label for="token">Enter your secret token here</label>
<input type="password" name="token" id="token">
<input type="submit" value="Login">
</form>
<h2>Register</h2>
<form action="/auth/user/register" method="post">
<label for="invite">Registration invite code</label>
<input type="text" name="invite" id="invite" value="YOUR_INVITE_CODE_HERE">
<input type="submit" value="Register">
</form>
Finally we can configure our reverse proxy to use the auth middleware to protect routes:
your.domain.tld { # Let users access login and register routes
reverse_proxy /auth/* :3000 # Protect the admin auth API
forward_auth /auth/admin/* :3000 {
uri /authorize?roles=admin
} # Protect anything that is under /protected
forward_auth /protected/* :3000 {
uri /authorize?roles=user&roles=admin
} # Serve the files in current directory
root * .
file_server
}
FAQ
What's the point of a subrequest-based authentication middleware?
Let's say you make a small website or service. But you want to restrict the usage of certain routes or actions to only certain groups of users.
One way to do this would be to implement the authentication yourself in your project. But handling authentication is very boring, a ton of boilerplate code and also very risky and hard to do well.
User <--> Reverse Proxy <--> Service
Another way could be to offload the authentication to a trusted authority (like with OAuth2 and OpenID Connect), those are the "sign-in with Google" kind of buttons. Those work, but again integrating them is still a lot of boilerplate code, and those systems tend to be pretty heavy and not always so easy to self-host which is why most people just depend on centralized solutions such as big corporations like Google or Microsoft.
User <--> Reverse Proxy <--> Service <--> OAuth2 Provider
\___________________________________________/
But there's a another way, you could use a proxy. So the requests pass through the proxy, if the user is authorized, they are then passed to the backend. This works, but it puts an additional burden on the authentication proxy since you probably have already a reverse proxy (like nginx or caddy), your reverse proxy will then pass the request to another reverse proxy which itself will eventually pass it to the backend. This is redundant, and can become more complicated to configure.
User <--> Reverse Proxy <--> Auth Proxy <--> Service
Finally, an another way is to use a subrequest authentication system. You simply tell your reverse proxy that when certain protected routes are requested, it should make a GET request (with a copy of the headers of the original request) to the authentication system, if the response is 200, it should let it through; and if the response is 401 it should do something else (like prevent access and redirect to login page or error page).
User <--> Reverse Proxy <--> Service
|
|
Auth Middleware
This system has the advantage of being very specific (the authentication system only has one responsibility) while being very versatile (you can pretty much configure the entire configuration in your reverse proxy directly). This is why this project can be as simple as ~200 lines of Rust and fit in under 300Kb.
How secure is the authentication?
If you want to implement authentication the most basic thing you're probably thinking of are login and register form with login and password field. And of course the hash of the password (with a secure, slow cryptographic function such as argon2id) is stored in the backend.
However, despite the hash, this is still very limited. Since the user chooses its password, and most users don't pick secure passwords (which should be fully random and unique) there's a wide range of protections to add on the system here to reduce the risk. Such as passwords constraints, multi-factor authentication, rate-limiting, etc. Those are very annoying to do yourself though, also this type of system usually depend on some private data such as the email, phone number, etc.
But there's actually a much easier way to handle authentication that completely circumvent a lot of those issues. Most of the issues come from one thing: the user chooses their password. Instead we could simply define a system where the server returns a secure completely random token (in the case of this system, a random 16 lowercase alphanumeric character string). This token is then used by the user to connect (and that's the only thing the user needs to connect).
Since the token is generated by the server it's a lot less sensitive since the user won't be able to set their password to something they reuse elsewhere. So if the database is compromised, only the user's account on that particular system is compromised. However that doesn't mean I want that token to be in cleartext in my database.
So, instead I store it as a hash, but since it's the only identifier using argon2id would be very inefficient (since it would require verifying the hash against every user entry, being very slow even with a little as 2 users). So instead I use sha256 which is very fast and doesn't have salt. Which is not an issue in my case since the token is fully random there's no way to guess the password other than pure bruteforce (good luck bruteforcing your way through when there are 7 958 661 109 946 400 884 391 936 possible combinaisons).
So the user identifier stored in the database is simply the hash of the user token in sha256. In order to login, the server takes the token provided by the user, hashes it using sha256 and checks if there's a match in the database, as simple as that.
An example of a secure service using this style of authentication system is Mullvad VPN.
How to prevent spam of registration
In usual systems this is prevented using some advanced systems of captchas and stuff. However those systems are far from fool proof and requires a lot more additional complexity on the system. However this is only required for global scale systems that require fully open registration.
This project aims to be for a smaller scale. You're not going to make the next Youtube or Facebook with this. But either way if you're trying to be the next Facebook... please stop.
The solution chosen here is to use invite codes. Each invite code is bound to an eventual deadline and a list of roles (roles are groups you can put users into to have a precise control over who has access to what). You simply have to provide the invite code to someone, they enter it in the registration form and click on "register". And boom ! The system returns them their token (to use to re-authenticate) and they are already logged in.
What to do if the user lost their token?
If the session is no longer valid and the user lost their token, in a traditional system a verification email would be sent to the user, with a link to let them restore their account. However this sort of system can get quite complex and also require the user giving some personal information at some point (such as an email or phone number).
A very interesting alternative is to ask for an email, and hash it in the database. When the user asks for an account recovery, they enter their recovery email for their account and the system will verify the hash of the recovery email. This way the personal information is nowhere to be found in the database since it only stores hashes. This is a system used by RiseUp. However it's still pretty complex and there's no way for the user to actually know that their data is being hashed properly.
But again here we're doing small scale, so the user probably can easily recontact the admin. Also the user could re-use an invite code if it hasn't expired to create a new account. So this is the very lazy, but efficient and social solution to this problem.
What database is used to store the data?
While most systems would use complex databases such as MySQL, PostgreSQL or even Sqlite. All those databases have the advantage of being able to handle complex datatypes and requests. But they also require more additional setup (or increased binary size) which I found undesirable.
This system has very simple datatypes (actually 3 : sessions, invites and users), so we can afford simply using a file for storing the data. Avoiding completely any external dependency.
Accessing the file for every read operation would not be very efficient (but honestly would still work fine in this small-scale context), so instead what is done here is simply that when the program is loaded, it loads the content of the file in memory and only writes back in the file when things needs to be modified.
What are the limitations of this system?
There are two major limitations I can see here.
The first limitation is that this is meant to work on small-scale. This wouldn't be so good at scaling for millions of users without some pretty big tradeoffs.
The second limitation is that it doesn't work with services that already have authentication built-in. This is more of a system to put on top of things you want to protect but don't have any form of authentication such as a static website or home-cooked apps/services you would build (this way you don't have to think about authentication at all when making it).
Why are requests in weird x-www-form-urlencoded format?
Most APIs indeed don't use this format and instead rely on JSON for both requests and responses. However I found it was sad because it means that in order to work from the client-side you need to use JavaScript to make the requests because <form> element in HTML doesn't support JSON.
That's why I've used x-www-form-urlencoded (which is the format used by HTML form elements), thanks to this you can interact with the API from your browser without ever needing JavaScript. Of course JS makes things more fancy but it's not strictly required and I like that.
That's also the reason why I used a cookie instead of an Authorization header like many APIs use. Authorization headers can only be set by JavaScript while cookies are handled automatically by the browser.
The construction field (it's 1AM and I will clean this readme later)
- this is only an api and doesn't include login pages
- any properties can be added to users
- users can be assigned roles by administrators