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");
const cookie1 = cookies.get("cookie1");
const entries = cookies.entries();
oslo/encoding
import { encodeBase64, decodeBase64 } from "oslo/encoding";
const textEncoder = new TextEncoder();
const encoded = encodeBase64(textEncoder.encode("hello world"));
const decoded = decodeBase64(encoded);
import { encodeBase64Url, decodeBase64Url } from "oslo/encoding";
import { encodeHex, decodeHex } from "oslo/encoding";
oslo/oauth2
import { createOAuth2AuthorizationUrl } from "oslo/oauth2";
const [url, state] = await createOAuth2AuthorizationUrl(
"https://github.com/login/oauth/authorize",
{
clientId,
redirectUri,
scope: ["user:email"]
}
);
setCookie("github_oauth_state", state, {
httpOnly: true,
path: "/",
maxAge: 60 * 60,
secure: true
});
redirect(url);
import { createOAuth2AuthorizationUrlWithPKCE } from "oslo/oauth2";
const [url, codeVerifier, state] = await createOAuth2AuthorizationUrlWithPKCE();
import {
verifyOAuth2State,
validateOAuth2AuthorizationCode,
OAuth2AccessTokenRequestError
} from "oslo/oauth2";
const storedState = getCookie("github_oauth_state");
const state = url.searchParams.get("state");
if (!verifyOAuth2State(storedState, state)) {
}
const code = url.searchParams.get("code");
if (!code) {
}
try {
const { accessToken, refreshToken } = await validateOAuth2AuthorizationCode<{
refreshToken: string;
}>(code, {
tokenEndpoint: "https://github.com/login/oauth/access_token",
clientId: this.config.clientId,
clientPassword: {
clientSecret: this.config.clientSecret,
authenticateWith: "client_secret"
}
});
} catch (e) {
if (e instanceof OAuth2AccessTokenRequestError) {
const { request, message, description } = e;
}
}
oslo/oauth2/providers
import { Github, Apple, Google } from "oslo/oauth2/providers";
const github = new Github({
clientId,
clientSecret,
scope: ["user:email"]
});
const [url, state] = await github.createAuthorizationURL();
const tokens = await validateOAuth2AuthorizationCode(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 { TOTP } from "oslo/otp";
import { TimeSpan } from "oslo";
const totp = new TOTP({
period: new TimeSpan(30, "s"),
digits: 6
});
const secret = new Uint8Array(20);
crypto.getRandomValues(secret);
const otp = await totp.generate(secret);
const validOTP = await totp.verify(otp, secret);
import { createKeyURI } from "oslo/otp";
const secret = new Uint8Array(20);
crypto.getRandomValues(secret);
const uri = createKeyURI({
type: "totp",
secret,
issuer: "My website",
accountName: "user@example.com",
period: new TimeSpan(30, "s"),
algorithm: "SHA-1",
digits: 6
});
const uri = createKeyURI({
type: "hotp",
secret,
issuer: "My website",
accountName: "user@example.com",
counter,
digits: 6
});
const qr = createQRCode(uri);
oslo/password
This module only works in Node.js. Use packages provided by your runtime for non-Node.js environment.
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);
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({
origin: request.headers.get("Origin"),
host: request.headers.get("Host")
});
const validRequestOrigin = verifyRequestOrigin({
origin: request.headers.get("Origin"),
host: request.url
});
if (!validRequestOrigin) {
return new Response(null, {
status: 400
});
}
verifyRequestOrigin({
origin: "https://example.com",
host: "example.com"
});
verifyRequestOrigin({
origin: "https://foo.example.com",
host: "bar.example.com",
allowedSubdomains: "*"
});
verifyRequestOrigin({
origin: "https://foo.example.com",
host: "bar.example.com",
allowedSubdomains: ["foo"]
});
verifyRequestOrigin({
origin: "https://example.com",
host: "foo.example.com",
allowedSubdomains: [null]
});
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({
name: "session",
secure: prod,
secret
});
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.isReusableToken(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
validateWebAuthnAttestationResponse()
does not validate attestation certificates.
import { validateWebAuthnAttestationResponse } from "oslo/webauthn";
try {
await validateWebAuthnAttestationResponse({
response: {
clientDataJSON,
authenticatorData
},
challenge,
origin: "http://localhost:3000"
});
} catch {
}
validateWebAuthnAssertionResponse()
currently only supports ECDSA using secp256k1 curve and SHA-256 (algorithm ID -7
).
import { validateWebAuthnAssertionResponse } from "oslo/webauthn";
try {
await validateWebAuthnAssertionResponse({
algorithm: "ES256K",
response: {
clientDataJSON,
authenticatorData,
signature
},
challenge,
publicKey,
origin: "http://localhost:3000"
});
} catch {
}