oslo
This package is highly experimental - use at your own risk
A collection of utilities for auth, including:
Aside from oslo/password
, every module works in any environment, including Node.js, Cloudflare Workers, Deno, and Bun.
Installation
npm i oslo
pnpm add oslo
yarn add oslo
oslo/cookie
import { serializeCookie } from "oslo/cookie";
const cookie = serializeCookie(name, value, {
expires: new Date(),
maxAge: 60 * 60,
path: "/",
httpOnly: true,
secure: true,
sameSite: "lax"
});
response.headers.set("Set-Cookie", cookie);
serializeCookie("this is fine =;", "this too =;");
import { parseCookieHeader } from "oslo/cookie";
const cookies = parseCookieHeader("cookie1=hello; cookie2=bye");
oslo/crypto
import { sha1, sha256, sha384, sha512 } from "oslo/crypto";
const data = new TextEncoder().encode("Hello world");
const hash = sha1(data);
import { HMAC } from "oslo/crypto";
const hs256 = new HMAC("SHA-256");
const key = await hs256.generateKey();
const data = new TextEncoder().encode("Hello world");
const signature = await hs256.sign(key, data);
const validSignature = await hs256.verify(key, signature, data);
import { ECDSA } from "oslo/crypto";
const es256 = new ECDSA("SHA-256", "P-256");
const key = await es256.generateKey();
const data = new TextEncoder().encode("Hello world");
const signature = await es256.sign(key, data);
const validSignature = await es256.verify(key, signature, data);
import { RSASSAPKCS1v1_5 } from "oslo/crypto";
const rs256 = new RSASSAPKCS1v1_5("SHA-256");
const key = await rs256.generateKey();
const data = new TextEncoder().encode("Hello world");
const signature = await rs256.sign(key, data);
const validSignature = await rs256.verify(key, signature, data);
import { RSAPSS } from "oslo/crypto";
const rsaPSS256 = new RSAPSS("SHA-256");
const key = await rsaPSS256.generateKey();
const data = new TextEncoder().encode("Hello world");
const signature = await rsaPSS256.sign(key, data);
const validSignature = await rsaPSS256.verify(key, signature, data);
oslo/encoding
import { encodeBase64, decodeBase64 } from "oslo/encoding";
const encoded = encodeBase64(new TextEncoder().encode("hello world"));
const decoded = decodeBase64(encoded);
import { encodeBase64url, decodeBase64url } from "oslo/encoding";
import { encodeHex, decodeHex } from "oslo/encoding";
import { encodeBase32, decodeBase32 } from "oslo/encoding";
oslo/oauth2
import { OAuth2Controller } from "oslo/oauth2";
const authorizeEndpoint = "https://github.com/login/oauth/authorize";
const tokenEndpoint = "https://github.com/login/oauth/access_token";
const oauth2Controller = new OAuth2Controller(clientId, authorizeEndpoint, tokenEndpoint, {
redirectURI: "http://localhost:3000/login/callback"
});
import { generateState, generateCodeVerifier } from "oslo/oauth2";
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = await createAuthorizationURL({
state,
scope: ["user:email"],
codeVerifier
});
import { verifyState, AccessTokenRequestError } from "oslo/oauth2";
if (!verifyState(storedState, state)) {
}
try {
const { accessToken, refreshToken } = await oauth2Controller.validateAuthorizationCode<{
refreshToken: string;
}>(code, {
credentials: clientSecret,
authenticateWith: "request_body",
codeVerifier
});
} catch (e) {
if (e instanceof AccessTokenRequestError) {
const { request, message, description } = e;
}
}
oslo/oauth2/providers
Some providers may require a code challenge and code verifier.
import { Github, Apple, Google } from "oslo/oauth2/providers";
const githubOAuth = new Github(clientId, clientSecret, {
scope: ["user:email"]
});
const url = await githubOAuth.createAuthorizationURL(state);
const tokens = await githubOAuth.validateAuthorizationCode(code);
oslo/oidc
import { parseIdToken } from "oslo/oidc";
const { sub, exp, email } = parseIdToken<{ email: string }>(idToken);
oslo/otp
import { generateHOTP } from "oslo/otp";
const secret = new Uint8Array(20);
crypto.getRandomValues(secret);
let counter = 0;
const otp = await generateHOTP(secret, counter);
const otp = await generateHOTP(secret, counter, 8);
import { TOTPController } from "oslo/otp";
import { TimeSpan } from "oslo";
const totpController = new TOTPController({
period: new TimeSpan(30, "s"),
digits: 6
});
const secret = new Uint8Array(20);
crypto.getRandomValues(secret);
const otp = await totpController.generate(secret);
const validOTP = await totpController.verify(otp, secret);
import { createHOTPKeyURI, createTOTPKeyURI } from "oslo/otp";
const secret = new Uint8Array(20);
crypto.getRandomValues(secret);
const issuer = "My website";
const accountName = "user@example.com";
const uri = createHOTPKeyURI(issuer, accountName, secret, {
counter: 0,
algorithm: "SHA-1",
digits: 6
});
const uri = createTOTPKeyURI(issuer, accountName, secret, {
period: new TimeSpan(30, "s"),
algorithm: "SHA-1",
digits: 6
});
oslo/password
Hash passwords with argon2id, scrypt, and bcrypt using the fastest package available for Node.js.
import { Argon2id, Scrypt, Bcrypt } from "oslo/password";
const argon2id = new Argon2id(options);
const hash = await argon2id.hash(password);
const matches = await argon2id.verify(hash, password);
This specific module only works in Node.js. See these packages for other runtimes:
oslo/random
All functions are cryptographically secure.
import { generateRandomString, alphabet } from "oslo/random";
const id = generateRandomString(16, alphabet("0-9", "a-z", "A-Z"));
const id = generateRandomString(16, alphabet("0-9", "a-z"));
const id = generateRandomString(16, alphabet("0-9"));
const id = generateRandomString(16, alphabet("0-9", "a-z", "A-Z", "-", "-"));
import { random } from "oslo/random";
const num = random();
import { generateRandomNumber } from "oslo/random";
const num = generateRandomNumber(0, 10);
oslo/request
CSRF protection.
import { verifyRequestOrigin } from "oslo/request";
const validRequestOrigin = verifyRequestOrigin(request.headers.get("Origin"), {
host: request.headers.get("Host")
});
const validRequestOrigin = verifyRequestOrigin(request.headers.get("Origin"), {
host: request.url
});
if (!validRequestOrigin) {
return new Response(null, {
status: 400
});
}
verifyRequestOrigin("https://example.com", "example.com");
verifyRequestOrigin("https://foo.example.com", "bar.example.com", {
allowedSubdomains: "*"
});
verifyRequestOrigin("https://foo.example.com", "bar.example.com", {
allowedSubdomains: ["foo"]
});
verifyRequestOrigin("https://example.com", "foo.example.com", {
allowBaseDomain: true
});
oslo/session
import { SessionController } from "oslo/session";
import { generateRandomString, alphabet } from "oslo/random";
import { TimeSpan } from "oslo";
import type { Session } from "oslo/session";
const sessionController = new SessionController(new TimeSpan(30, "d"));
async function validateSession(sessionId: string): Promise<Session | null> {
const databaseSession = await db.getSession(sessionId);
if (!databaseSession) {
return null;
}
const session = sessionController.validateSessionState(sessionId, databaseSession.expires);
if (!session) {
await db.deleteSession(sessionId);
return null;
}
if (session.fresh) {
await db.updateSession(session.sessionId, {
expires: session.expiresAt
});
}
return session;
}
async function createSession(): Promise<Session> {
const sessionId = generateRandomString(41, alphabet("a-z", "A-Z", "0-9"));
const session = sessionController.createSession(sessionId);
await db.insertSession({
id: session.sessionId,
expires: session.expiresAt
});
return session;
}
const sessionCookieController = sessionController.sessionCookieController(cookieName, {
expires: true,
secure: true,
path: "/",
domain,
sameSite
});
const session = await createSession();
const cookie = sessionCookieController.createSessionCookie(session.sessionId);
const sessionId = sessionCookieController.parseCookieHeader(headers.get("Cookie"));
const sessionId = cookies.get(sessionCookieController.cookieName);
if (!sessionId) {
}
const session = await validateSession(sessionId);
if (session.fresh) {
const cookie = sessionCookieController.createSessionCookie(session.sessionId);
}
const cookie = sessionCookieController.createBlankSessionCookie();
oslo/token
For email verification tokens and password reset tokens.
import { VerificationTokenController } from "oslo/token";
import { isWithinExpirationDate } from "oslo";
import { generateRandomString, alphabet } from "oslo/random";
import type { Token } from "oslo/token";
const verificationTokenController = new VerificationTokenController(new TimeSpan(2, "h"));
async function generatePasswordResetToken(userId: string): Promise<Token> {
const storedUserTokens = await db
.table("password_reset_token")
.where("user_id", "=", userId)
.getAll();
if (storedUserTokens.length > 0) {
const reusableStoredToken = storedUserTokens.find((token) => {
return verificationTokenController.isTokenReusable(token.expires);
});
if (reusableStoredToken) return reusableStoredToken.id;
}
const token = verificationTokenController.createToken(
generateRandomString(63, alphabet("a-z", "0-9")),
userId
);
await db
.insertInto("password_reset_token")
.values({
id: token.value,
expires: token.expiresAt,
user_id: token.userId
})
.executeTakeFirst();
return token;
}
async function validatePasswordResetToken(token: string): Promise<string> {
const storedToken = await db.transaction().execute(async (trx) => {
const storedToken = await trx.table("password_reset_token").where("id", "=", token).get();
if (!storedToken) return null;
await trx.table("password_reset_token").where("id", "=", token).delete();
return storedToken;
});
if (!storedToken) throw new Error("Invalid token");
if (!isWithinExpirationDate(storedToken.expires)) {
throw new Error("Expired token");
}
return storedToken.user_id;
}
const token = await generatePasswordResetToken(session.userId);
await sendEmail(`http://localhost:3000/reset-password/${token.value}`);
oslo/webauthn
validateAttestationResponse()
does not validate attestation certificates. validateAssertionResponse()
currently only supports ECDSA using secp256k1 curve and SHA-256 (algorithm ID -7
).
import { WebAuthnController } from "oslo/webauthn";
const webauthn = new WebAuthnController("http://localhost:3000");
try {
const response: AttestationResponse = {
clientDataJSON,
authenticatorData
};
await webauthn.validateAttestationResponse(response, challenge);
} catch {
}
try {
const response: AssertionResponse = {
clientDataJSON,
authenticatorData,
signature
};
await webauthn.validateAssertionResponse(
"ES256",
response,
publicKey,
challenge
);
} catch {
}