Skip to content

Instantly share code, notes, and snippets.

@MKRhere
Last active August 1, 2022 10:10
Show Gist options
  • Save MKRhere/c0fe88c7f15781e40e4b30a406c7fc7d to your computer and use it in GitHub Desktop.
Save MKRhere/c0fe88c7f15781e40e4b30a406c7fc7d to your computer and use it in GitHub Desktop.
how to do auth

how to do auth

While seemingly simple, I see this question often, so I'm sharing my auth setup here, and how you could improve on it.

There are 4 parts to this: register, login, authentication, and logout.

Your register route takes a username & password, hashes the password and stores in the db. For reference, this would be a very minimal user schema:

type Roles = "USER" | "ADMIN";

type Session = {
	// you could store additional information about login time, useragent, etc
	token: string;
}

type User = {
	email: string;
	password: string;
	role: Roles;
	sessions: Session[];
	// ...
}

When the user logs in for the first time, a session token is created and pushed to the sessions array (you can rotate the array so there's always a maximum number of sessions per user). This token is also sent to the client as a httpOnly cookie set to expire in, let's say a week. Additionally, Secure should be set in production only.

Set-Cookie: Authorisation=a3fWa...; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly

When making API requests with fetch, set credentials: "include", or withCredentials: true if using axios. The browser will automatically include the token when making requests.

On your /logout route, you simply unset the cookie we set before and delete it from the db.

Now for the juicy part. Authentication on other routes. If using express or similar http library, we can have a middleware similar to this:

function auth (roles: Roles[]): Handler => async function (req, res, next) {
	const token = req.cookies["Authorisation"];

	if (!token) {
		res.status(401);
		return res.send({ msg: "You're not logged in." });
	}

	const user = await database.User.findOne({ "sessions.token": token });

	if (!user) {
		res.status(401);
		return res.send({ msg: "Malformed token" });
	}

	if (roles.includes(user.role)) {
		// All good
		res.locals.user = user;
		next();
	} else {
		// User doesn't have necessary role
		res.status(403);
		res.send({ msg: "No access to this resource" });
	}
};

Now we simply use this middleware like so:

app.post("/api/article", auth(["ADMIN"]), (req, res) => ...);

If the user isn't signed in, or doesn't have the admin role, they'll be turned away.

Can we do better?

Yes, we could, if you think the benefits are worthwhile. You could wrap the token in a JWT before setting as cookie. This would let us verify if the token is valid at all, without having to look up the database. It has the small benefit that an attacker trying to access a user by bruteforcing a token will be unable to sign his token.

But in my experience, it's unlikely we ever need to process a request without looking up the user object. You could enclose more user data in the JWT, for example user.role, but we lose the ability to change user role in realtime. You'd need to wait for the user to logout and back in, or for the token to expire and they're forced to log in again. So JWT will add a layer of security to your application, but if you agree with me so far, it wouldn't be your only way to authenticate. But read on.

But why not use JWTs the normal way?

I'm glad you asked. Because while JWTs are great for verifying identity statelessly, it's a poor solution to auth. At any time, the server has no knowledge of how many sessions are "logged in", it's impossible to forcefully log out a (compromised?) user session without changing the JWT secret, which logs everyone out.

A user wouldn't be able to look at his sessions and delete them. User data stored in the JWT won't be realtime, and will need a relogin or token refresh to update. Even user logouts would be a lie, because it only deletes the JWT token from the client, while it's still valid. If an attacker somehow owned this token, he'd have unfettered access until it expires.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment