Modern, OpenID Connect compatible authentication.
// ...
let LibAuth = require("libauth");
let libauth = LibAuth.create(issuer, privkey, {
cookies: { path: "/api/authn/" },
});
// ...
app.post("/api/authn/session/credentials", setCookieByCredentials);
app.post("/api/authn/session/id_token", sendIdTokenBySession);
app.post("/api/authn/access_token", sendAccessTokenByIdToken);
app.delete("/api/authn/session", revokeCookieAndIdToken);
app.use("/.well-known/openid-configuration", libauth.wellKnownOidc());
app.use("/.well-known/jwks.json", libauth.wellKnownJwks());
- Standards-based session management (localStorage not necessary)
- Long-lived refresh tokens (default 30d)
- Long-lived session cookies (default 30d)
- Short-lived id & access tokens (default 1d, 15m)
- Logout (expire refresh cookie)
- Supports common login strategies
- Credentials-based login (HTTP Basic Auth)
- OIDC (OpenID Connect, OAuth2) login (Google Sign In, etc)
- Challenge-Response (Magic Link)
- Idiomatic, Composable Express.js Routing
- Email & SMS Verification flow (bring-your-own-mailer)
- No need for
localStorage
!
- Installation
- Node.js
- libauth
- Philosophy
- Usage
Install Node.js:
curl https://webinstall.dev/node@v16 | bash
export PATH="$HOME/.local/opt/node/bin:$PATH"
Install libauth:
npm install --save libauth@v0
Note: The v1 API is not yet locked. Some names may change. Any changes from v0.90.x on will be noted in migration notes.
The goal of LibAuth is to minimize magic (anything difficult to understand or configure), and maximize control, without sacrificing ease-of-use, convenience, or security.
To do this we require more copy-and-paste boilerplate than other auth libraries - with the upside is that it's all just normal, easy-to-replace middleware - hopefully nothing unexpected or constraining.
You'll also notice that we try to use the proper, official technical language rather than potentially ambiguous sugar-coated terms (for example: 'cookie' or 'token' when specificity is required, or 'session' when it could be either).
There's a few keys, secrets, and salts that you'll need. Here's how you can generate them:
You can generate a .env
with the required secrets all at once:
npx libauth@v0 envs ./.env
.env
:
COOKIE_SECRET=xxxx
MAGIC_SALT=xxxx
PRIVATE_KEY='{"d": "xxxx"}'
Or you can generate them one-by-one with the libauth commands:
npx libauth@v0 privkey
npx libauth@v0 rnd
which are the same as running the following:
./node_modules/libauth/bin/libauth.js privkey
./node_modules/libauth/bin/libauth.js rnd
Each of these should be included in your .env
file:
- Generate the
PRIVATE_KEY
:echo "PRIVATE_KEY='$( npx libauth@v0 privkey )'" >> .env
- Generate the
COOKIE_SECRET
:echo "COOKIE_SECRET=$( npx libauth@v0 rnd 16 )" >> .env
- Generate the
MAGIC_SALT
:echo "MAGIC_SALT=$( npx libauth@v0 rnd 16 )" >> .env
You could also use keypairs and
openssl rand -base64 16
.
"use strict";
let FsSync = require("fs");
let issuer = process.env.BASE_URL || `http://localhost:${process.env.PORT}`;
let privkey = JSON.parse(FsSync.readFileSync("./key.jwk.json", "utf8"));
let bodyParser = require("body-parser");
app.use("/api/authn", bodyParser.json({ limit: "100kb" }));
let cookieParser = require("cookie-parser");
let cookieSecret = process.env.COOKIE_SECRET;
app.use("/api/authn/session", cookieParser(cookieSecret));
let authRoutes = require("./auth-routes.js").create(issuer, privkey, {
cookies: { path: "/api/authn/session/", sameSite: "strict" },
});
app.post("/api/authn/session/credentials", authRoutes.setCookieByCredentials);
app.post("/api/authn/session/id_token", authRoutes.sendIdTokenBySession);
app.delete("/api/authn/session", authRoutes.revokeCookieAndIdToken);
app.post("/api/authn/access_token", authRoutes.sendAccessTokenByIdToken);
app.get("/.well-known/openid-configuration", authRoutes.wellKnownOidc);
app.get("/.well-known/jwks.json", authRoutes.wellKnownJwks);
module.exports = app;
"use strict";
let AuthRoutes = module.exports;
let LibAuth = require("libauth");
AuthRoutes.create = function () {
let authRoutes = {};
let libauth = LibAuth.create(issuer, privkey, {
cookies: {
path: "/api/authn/",
sameSite: "strict",
},
/*
refreshCookiePath: "/api/authn/",
accessCookiePath: "/api/assets/",
*/
});
// Create session by login credentials
AuthRoutes.setCookieByCredentials = [
libauth.readCredentials(),
MyDB.getUserClaimsByPassword,
libauth.newSession(),
libauth.initClaims(),
libauth.initTokens(),
libauth.initCookie(),
MyDB.expireCurrentSession,
MyDB.saveNewSession,
libauth.setCookieHeader(),
libauth.sendTokens(),
];
// Refresh ID Token via Session
AuthRoutes.sendIdTokenBySession = [
libauth.requireCookie(),
MyDB.getUserClaimsBySub,
libauth.initClaims({ idClaims: {} }),
libauth.initTokens(),
libauth.sendTokens(),
];
// Exchange Access Token via ID Token
AuthRoutes.sendAccessTokenByIdToken = [
libauth.requireBearerClaims(),
MyDB.getUserClaimsBySub,
libauth.initClaims({ accessClaims: {} }),
libauth.initTokens(),
libauth.sendTokens(),
];
// Logout (delete session cookie)
AuthRoutes.revokeCookieAndIdToken = [
libauth.readCookie(),
MyDB.expireCurrentSession,
libauth.expireCookie(),
libauth.sendOk({ success: true }),
libauth.sendError({ success: true }),
];
AuthRoutes.wellKnownOidc = libauth.wellKnownOidc();
AuthRoutes.wellKnownJwks = libauth.wellKnownJwks();
return authRoutes;
};
"use strict";
require("dotenv").config({ path: ".env" });
let Http = require("http");
let Express = require("express");
let server = Express();
let app = new Express.Router();
let port = process.env.PORT || 3000;
Http.createServer(server).listen(port, function () {
/* jshint validthis:true */
console.info("Listening on", this.address());
});
Handling the following strategies:
-
oidc
- ex: Facebook Connect, Google Sign In, Microsoft Live -
credentials
- bespoke, specified by you- Username / Password
- API Key
-
challenge
- a.k.a. "verification email" or "Magic Link" ( or SMS code) -
refresh
- to refresh anid_token
via refresh token cookie -
exchange
- to exchange anid_token
for anaccess_token
- JWK
Name | Status | Message (truncated) |
---|---|---|
E_CODE_NOT_FOUND | 404 | That verification code isn't valid. It might ... |
E_CODE_INVALID | 400|403 | That verification code isn't valid. It might ... |
E_CODE_REDEEMED | 400 | That verification code has already been used ... |
E_CODE_RETRY | 400 | That verification code isn't correct. It may ... |
E_OIDC_UNVERIFIED_IDENTIFIER | 400 | You cannot use the identifier associated with... |
E_SESSION_INVALID | 400 | Missing or invalid cookie session. Please log... |
E_SUSPICIOUS_REQUEST | 400 | Something suspicious is going on - as if ther... |
E_SUSPICIOUS_TOKEN | 400 | Something suspicious is going on - the given ... |
E_DEVELOPER_ERROR | 422 | Oops! One of the programmers made a mistake. ... |
" -> WRONG_TOKEN_TYPE | 422 | the HTTP Authorization was not given in a sup... |
" -> MISSING_TOKEN | 401 | the required authorization token was not prov... |
Term | Meaning |
---|---|
JWS | A decoded JWT (non-compact JWS), or JSON Web Signature |
JWT | A compact (or encoded) JWS, or JSON Web Token |
id_token |
A JWT with information about the user, such given_name |
access_token |
A JWT with information about an account or resource |
refresh_token |
A long-lived JWT, stored in a session cookie (or config file) |
amr |
The list of methods used for Multi-Factor Authentication |
acr |
For specifying LoA Profiles (mostly useless) |
- directed flow of data
libauth
passes data to you throughreq.authn
via middleware- You pass data to
libauth
by POST or by calling functions
- Live Recordings of the making of this project
- Express Cookies Cheat Sheet
- How to add Google Sign In
- How many Bits of Entropy per Character in...
This code was written live, in front of a combined YouTube & Twitch audience.
If you want to see all 40+ hours of painstaking coding... here ya go:
https://www.youtube.com/playlist?list=PLxki0D-ilnqYmidRxvrQoF2jX67wH5OS0