@restingowlorg/owlauth
v1.2.0
Published
owl-auth: OWASP-informed authentication primitives for Node.js
Readme
owlauth
Open-source OWASP-aligned authentication and account-security library for Node.js.
owlauth, published as @restingowlorg/owlauth, gives your Node.js app the core pieces of authentication: credentials login, magic links, password checks, and security-focused audit logging. It works with PostgreSQL and MongoDB and stays out of your framework.
- Package:
@restingowlorg/owlauth - Latest stable tag:
latest - Prerelease tag:
next - Install:
npm install @restingowlorg/owlauth - Developer guide: docs/DEVELOPER_GUIDE.md
What You Get
- Credentials authentication: Sign up users, log them in, and rotate passwords through one library surface.
- Passwordless magic links: Request, verify, and consume single-use login tokens.
- PostgreSQL and MongoDB adapters: Use the same auth API across two common persistence stacks.
- Password hygiene controls: Enforce minimum strength with
zxcvbn, reject context-based weak passwords, and check candidate passwords against Have I Been Pwned using the k-anonymity API. - Security-focused logging: Audit events are logged with built-in masking for sensitive fields such as passwords, tokens, secrets, cookies, and authorization data.
- Request tracing support: Pass a
correlationIdthrough auth operations to align library logs with your application logs. - Pluggable cryptography: Swap the default crypto adapter if your stack requires a different hashing or token strategy.
- Typed, predictable results: Every auth method returns a consistent
AuthResult<T>shape.
Support Matrix
| Area | Current Support | | ------------- | ----------------------- | | Runtime | Node.js 18+ | | Language | TypeScript, JavaScript | | Module output | CommonJS | | Databases | PostgreSQL, MongoDB | | Auth flows | Credentials, Magic Link |
Installation
npm install @restingowlorg/owlauthQuick Start
import { createAuthManager, PostgresAdapter } from "@restingowlorg/owlauth";
const auth = await createAuthManager({
adapter: new PostgresAdapter({
postgresUrl: process.env.POSTGRES_URL!,
userTableName: "users",
magicLinkTableName: "magic_links"
}),
authTypes: ["credentials", "magicLink"],
blockedPasswords: ["company-name", "product-name"],
pwnedPasswordFailClosed: true,
customMaskingKeys: ["apiKey", "accessToken"]
});
const signup = await auth.credentials.signup(
"[email protected]",
"engineer01",
"CorrectHorseBatteryStaple!2026",
{ correlationId: "req_123" }
);
if (signup.success) {
console.log(signup.data.user.email);
}
await auth.disconnectDB();Database Adapters
PostgreSQL
import { createAuthManager, PostgresAdapter } from "@restingowlorg/owlauth";
const auth = await createAuthManager({
adapter: new PostgresAdapter({
postgresUrl: process.env.POSTGRES_URL!,
userTableName: "users",
userSchema: "public",
magicLinkTableName: "magic_links",
magicLinkSchema: "public"
}),
authTypes: ["credentials", "magicLink"]
});MongoDB
import { createAuthManager, MongoAdapter } from "@restingowlorg/owlauth";
const auth = await createAuthManager({
adapter: new MongoAdapter({
mongoUri: process.env.MONGO_URI!,
userCollectionName: "users",
magicLinkCollectionName: "magic_links"
}),
authTypes: ["credentials", "magicLink"]
});Cryptography
owlauth ships with a default BcryptAdapter (10 rounds). You can customize it or provide your own implementation of ICryptoAdapter.
Customizing Default Crypto
import { createAuthManager, BcryptAdapter } from "@restingowlorg/owlauth";
const auth = await createAuthManager({
// ... rest of config
cryptoAdapter: new BcryptAdapter()
});Implementing Your Own
If you need Argon2, PBKDF2, or a custom token strategy, implement the ICryptoAdapter interface:
import { ICryptoAdapter } from "@restingowlorg/owlauth";
class MyCrypto implements ICryptoAdapter {
async hashPassword(p: string) {
/* ... */
}
async verifyPassword(p: string, h: string) {
/* ... */
}
generateToken() {
/* ... */
}
async hashToken(t: string) {
/* ... */
}
async verifyToken(t: string, h: string) {
/* ... */
}
}
const auth = await createAuthManager({
// ...
cryptoAdapter: new MyCrypto()
});Core Usage
Credentials Flow
import {
AuthResult,
SignupResult,
LoginResult,
ChangePasswordResult
} from "@restingowlorg/owlauth";
const signup: AuthResult<SignupResult> = await auth.credentials.signup(
"[email protected]",
"engineer01",
"CorrectHorseBatteryStaple!2026"
);
const login: AuthResult<LoginResult> = await auth.credentials.login(
"[email protected]",
"CorrectHorseBatteryStaple!2026",
{
correlationId: "req_login_001"
}
);
const passwordChange: AuthResult<ChangePasswordResult> = await auth.credentials.changePassword(
"user_id123456",
"current_strong_password",
"new_strong_password_example",
{
correlationId: "req_password_001"
}
);Magic Link Flow
import {
AuthResult,
RequestMagicLinkResult,
VerifyMagicLinkResult,
ConsumeMagicLinkResult
} from "@restingowlorg/owlauth";
const requested: AuthResult<RequestMagicLinkResult> = await auth.magicLink.request(
"[email protected]",
{ correlationId: "req_magic_001" }
);
if (requested.success) {
const token = requested.data;
const verified: AuthResult<VerifyMagicLinkResult> = await auth.magicLink.verify(token);
const consumed: AuthResult<ConsumeMagicLinkResult> = await auth.magicLink.consume(token);
}request() returns a composite token string in the format {recordId}.{rawToken}. Both parts are required — verify() and consume() expect the full composite value as-is. Putting it in a URL, sending the email, and handling delivery is your application's job. owlauth does not touch any of that.
Note: The
recordIdsegment is the database record's primary key. It is not sensitive, but treat the full composite token as a secret: it grants one-time login access and must only be transmitted over TLS.
Configuration Options
Shared Options
| Option | Type | Purpose |
| ------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| authTypes | ("credentials" \| "magicLink")[] | Enables one or both supported auth flows. Defaults to credentials only. |
| blockedPasswords | string[] | Rejects passwords containing the user's email, username, or any supplied blocked terms. |
| magicLinkBaseUrl | string | Base URL for magic link emails. When set, request() returns a ready-to-use URL in the format {baseUrl}?token={token}. When omitted, the raw composite token is returned for manual URL construction. |
| cryptoAdapter | ICryptoAdapter | Replaces the default bcrypt-based crypto implementation. |
| customMaskingKeys | string[] | Adds case-insensitive keys to the audit logger masking list. |
| pwnedPasswordFailClosed | boolean | Rejects signups and password changes when the breached-password API cannot be reached. |
| usernameValidator | (username: string) => boolean | Overrides the default username validation rule. Default: 3–20 chars, alphanumeric + underscore only (/^[a-zA-Z0-9_]{3,20}$/). Return true to accept, false to reject. |
Note: The built-in
PostgresAdapterandMongoAdapterimplementfindByUsernameand enforce username uniqueness at signup. If you supply a customUserRepositorythat does not implementfindByUsername, duplicate-username detection is skipped silently. Implement the method if your application requires unique usernames.
Method-Level Options
signup():blockedPasswords,pwnedPasswordFailClosed,correlationIdlogin():correlationIdchangePassword():blockedPasswords,pwnedPasswordFailClosed,correlationIdmagicLink.request():correlationIdmagicLink.verify():correlationIdmagicLink.consume():correlationId
Response Model
Every public method returns the same envelope:
type AuthResult<T = unknown> =
| { success: true; data: T; httpCode: number; message: string }
| { success: false; data?: undefined; httpCode: number; message: string };Result payloads are:
SignupResult:{ user: SafeUser }— whereSafeUser = { id, email, username }LoginResult:{ user: SafeUser }ChangePasswordResult:{ user: SafeUser ; tokensInvalidated: boolean}RequestMagicLinkResult:string— whenmagicLinkBaseUrlis configured: full URL"{baseUrl}?token={recordId}.{rawToken}"ready to embed in an email. WithoutmagicLinkBaseUrl: raw composite"{recordId}.{rawToken}"for manual URL construction. Pass the token portion directly toverify()andconsume().VerifyMagicLinkResult:{ isValid: boolean; userId: UserId; lookupKey: string; }ConsumeMagicLinkResult:{ userId: UserId }
SafeUser and UserId are exported directly from the package and can be imported for type annotations:
import type { SafeUser, UserId } from "@restingowlorg/owlauth";OWASP Alignment
Here's exactly what owlauth does, and where each decision comes from. Every control is traced back to the OWASP Authentication Cheat Sheet, ASVS 5.0.0, or OWASP Top 10:2025.
Password Security
| Control | What the library does | OWASP reference |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Password strength scoring | Candidate passwords are scored with @zxcvbn-ts/core at signup and on every password change. Scores below 3 of 4 are rejected. | Auth Cheat Sheet — Implement Proper Password Strength Controls |
| Breached password check | The Have I Been Pwned Pwned Passwords API is queried using the k-anonymity range method. Only the first 5 characters of the SHA-1 hash are transmitted; the raw password never leaves the process. | Auth Cheat Sheet — Block previously breached passwords |
| Context-aware blocking | Passwords are rejected if they contain the user's email local part, username, or any caller-supplied blocked terms, preventing context-guessable passwords. | NIST SP 800-63B § 5.1.1.2, context-specific word verification |
| Fail-closed breach check | When pwnedPasswordFailClosed: true, a network error on the breach API causes the request to fail rather than silently pass. | Defense-in-depth, fail-safe defaults |
Credential Storage
| Control | What the library does | OWASP reference |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Adaptive password hashing | Passwords are hashed with bcrypt at 10 salt rounds via the built-in BcryptAdapter. | OWASP Password Storage Cheat Sheet, use bcrypt |
| Pluggable crypto adapter | The ICryptoAdapter interface allows swapping the default bcrypt implementation for any alternative hashing or token strategy without changing the auth API surface. | ASVS 5.0, algorithm agility |
Authentication Logic
| Control | What the library does | OWASP reference |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Generic login error messages | Login failure returns "Invalid credentials." regardless of whether the email is unknown or the password is wrong, preventing user enumeration. Magic link request() always returns 200 with a neutral message ("If this email is registered, a magic link has been sent.") regardless of whether the email exists, preventing email enumeration via the passwordless flow. | Auth Cheat Sheet, Authentication and Error Messages |
| Current password re-verification | changePassword() requires the caller to supply and verify the current password before a new one is accepted, preventing silent takeover through a hijacked session. | Auth Cheat Sheet, Change Password Feature |
Passwordless Authentication
| Control | What the library does | OWASP reference |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cryptographically secure token generation | Magic link tokens are produced with crypto.randomBytes(32) from Node.js's built-in CSPRNG. | OWASP Forgot Password Cheat Sheet, use a cryptographically random value |
| Token hashing at rest | The raw token is never stored. Only the SHA-256 hash of the token is persisted using a timing-safe comparison; the database contains no recoverable plaintext. | OWASP Forgot Password Cheat Sheet, hash tokens before storage |
| Short expiry window | Tokens expire 15 minutes after issuance. | OWASP Forgot Password Cheat Sheet, use a short token lifetime |
| Single-use with prior invalidation | Requesting a new magic link immediately invalidates all previous active tokens for that user. Consumed tokens cannot be reused. | OWASP Forgot Password Cheat Sheet, single-use tokens |
Security Logging and Audit Trail
| Control | What the library does | OWASP reference |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Security event logging | Every auth operation, signup, login, password change, and all magic link steps, emits a structured audit event with event type, email, outcome, and reason. | Auth Cheat Sheet, Logging and Monitoring; A09:2025 Security Logging and Alerting Failures |
| Sensitive field masking | The audit logger automatically redacts values at keys matching password, token, secret, authorization, cookie, and apikey. Callers can extend this with customMaskingKeys. | ASVS 5.0, do not log sensitive data;OWASP Logging Cheat Sheet |
| Correlation ID propagation | Every auth method accepts an optional correlationId. When supplied, it is included in all log output for that operation, enabling trace correlation with application-level logs. | OWASP Logging Cheat Sheet, include trace identifiers |
Security Notes
The table above covers what this library actually does. It's not an OWASP certification, and it won't make your app ASVS-compliant on its own. You still need to handle:
- TLS and secure transport
- secure email delivery for magic links
- rate limiting, brute-force protection, and account lockout
- session management
- CSRF defenses where relevant
- account verification and recovery workflows
- MFA or passkeys if the risk model requires them
- authorization and role enforcement
Roadmap
owlauth is part of a wider RestingOwl effort focused on building secure-by-default tooling for various technology stacks, not limited to Node.js.
Here's what's coming next:
- More application stacks: First-party integrations for Express, Fastify, NestJS, Next.js, and serverless Node runtimes
- More data stores: Additional adapters for MySQL, SQLite, DynamoDB, and other operationally common backends
- Stronger auth options: WebAuthn, passkeys, TOTP-based MFA, and recovery-oriented flows
- Operational hardening: Built-in rate limiting hooks, lockout strategies, and safer recovery patterns
- Broader RestingOwl package family: Adjacent packages for rate limiting, input sanitization, audit logging, secrets management, and CSRF protection
