@lcdev/auth
v1.1.1
Published
Common authentication mechanism for koa applications
Downloads
4
Keywords
Readme
Launchcode Auth Package
Common authentication mechanism for backend services. Uses a straightforward JWT implementation with a token blacklist for logout/invalidating.
yarn add @lcdev/auth@VERSION
If you're not familiar, you should be at least familiar with:
In short:
- We use JWTs for authenticating every user's identity
- They are stateless, so we don't need to store any sessions ourselves
- We can embed data into JWTs, avoiding costly database calls for user info
- We can trust data embedded into JWTs, because we're signing them with secret keys
- We blacklist individual JWTs based off of a uuid in the JWT data
- A more complex blacklisting scheme is easily possible (like by JWT data, eg. User ID)
Quick Start
There are two steps to integration. Login and normal authentication. We can start with the common one, authentication.
import { createAuthStrategy, Cache } from '@lcdev/auth';
import { createAuthCache } from '@lcdev/auth-redis-blacklist';
// here, we define the type of what we expect to be in all JWT tokens
interface JWT {
// it's very typical to see things like userID, user role, and maybe some preferences here
userID: number;
}
const cache = createAuthCache(redisConnection, 'some-token-blacklisting-key');
// most of this would normally come from app-config
const config = {
expiry: 60, // seconds
secrets: ["private-signing-key"], // this is the secret we sign tokens with!
blacklistClearingInterval: 120, // seconds, how often do we go back and clear out old blacklist entries
// optional: issuer, audience, debug, etc.
};
const auth = createAuthStrategy(cache, config);
So now we have this 'auth' object. Normally, we'd centralize the different logic here (eg. cache/redis, config, jwt definition). Once we've created the strategy, we can use it in routes to protect them.
// assuming you're using @lcdev/router
route({
path: '/protected-route',
method: HttpMethod.GET,
// this is the key point - `authenticate` is middleware that will prevent bad or missing tokens from going through
middleware: [auth.authenticate()],
async action(ctx) {
// in here, we get 'token', 'jwt' in ctx.state if need be (you'll need to cast `jwt` into your JWT type)
return { sensitiveInfo: true };
},
}),
How are tokens passed? An HTTP Authorization
header with Bearer {tokenHere}
.
So when a client calls the API, it needs to include that header. There are config
options for cookies and others, if you require.
Not so hard right? Only last part is user login, which is of course a tiny bit more involved.
Instead of createAuthStrategy
, we'll use createAuthLoginStrategy
. It's a superset of createAuthStrategy
, so feel free to
reuse the strategy for both use cases.
import { compare } from 'bcrypt';
import { createAuthLoginStrategy, FetchLogin, CheckLogin } from '@lcdev/auth';
const {
authenticate,
login,
logout,
newToken,
} = createAuthLoginStrategy<JWT>(
cache,
config,
// fetch the user by username here
async (username) => {
const user = await User.query().where({ username });
if (!user) {
throw { message: 'no user found', status: 401 };
}
return user;
},
// compare the given plaintext password with your stored hash
async (user, password) => {
return compare(password, user.password);
},
);
A login and logout route would look pretty similar to our earlier example.
route({
path: '/login',
method: HttpMethod.POST,
// login verifies the user identity, and newToken generates a normal JWT
middleware: [login(), newToken()],
async action(ctx) {
const { token, expiry } = ctx.state;
// token is the raw JWT string, expiry is a date
return { token, expiry };
},
}),
route({
path: '/logout',
method: HttpMethod.POST,
// the logout middleware adds this token to the blacklist, until it expires
middleware: [logout()],
async action(ctx) {
return {};
},
}),
Another other useful pattern is refreshing a given token:
route({
path: '/refresh',
method: HttpMethod.POST,
// authenticate verifies the user identity, and newToken generates a new JWT
middleware: [authenticate(), newToken()],
async action(ctx) {
const { token, expiry } = ctx.state;
return { token, expiry };
},
}),
Of course, it's useful to see a real example.