
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
A TypeScript OIDC authentication library with support for JWT and opaque tokens
A TypeScript library for handling OIDC authentication with support for both JWT and opaque tokens, intelligent caching, and customizable storage adapters.
DefAuth v2.0 introduces three major breaking changes that require code updates:
Authenticator → DefauthThe main class has been renamed from Authenticator to Defauth to avoid naming conflicts with user implementations.
Before (v1.x)
import { Authenticator, AuthenticatorConfig } from 'defauth';
const auth = new Authenticator(config);
// or
const auth = await Authenticator.create(config);
After (v2.0+)
import { Defauth, DefauthConfig } from 'defauth';
const auth = await Defauth.create(config);
Defauth.create() OnlyThe constructor is now private and can only be accessed through the Defauth.create() static method. This ensures all instances are properly initialized.
Before (v1.x)
// ❌ Old way - constructor (synchronous)
const auth = new Authenticator({
issuer: 'https://example.com',
clientId: 'client-id',
clientSecret: 'client-secret'
});
// Had to wait for async initialization or handle race conditions
const user = await auth.getUser(token);
After (v2.0+)
// ✅ New way - async factory method (constructor is private)
const auth = await Defauth.create({
issuer: 'https://example.com',
clientId: 'client-id',
clientSecret: 'client-secret'
});
// Ready to use immediately, no race conditions
const user = await auth.getUser(token);
DefAuthError → DefauthErrorThe base error class has been renamed from DefAuthError to DefauthError for naming consistency.
Before (v1.x)
import { DefAuthError } from 'defauth';
try {
const user = await auth.getUser(token);
} catch (error) {
if (error instanceof DefAuthError) {
// Handle DefAuth-specific errors
}
}
After (v2.0+)
import { DefauthError } from 'defauth';
try {
const user = await auth.getUser(token);
} catch (error) {
if (error instanceof DefauthError) {
// Handle Defauth-specific errors
}
}
Authenticator to Defauth in all import statementsAuthenticatorConfig to DefauthConfig if using TypeScriptDefAuthError to DefauthError in import statements and error handling codenew Authenticator() or new Defauth() to await Defauth.create() (constructor is now private)Authenticator.create() to Defauth.create()authenticator → defauth)Defauth → DefauthError)npm install defauth
import { Defauth } from 'defauth';
// Create and initialize an authenticator (recommended approach)
const auth = await Defauth.create({
issuer: 'https://your-oidc-provider.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret' // Optional for public clients
});
// Get user from any token type
const user = await auth.getUser(token);
// Force introspection (for high-security scenarios)
const validatedUser = await auth.getUser(token, { forceIntrospection: true });
// Custom validation (e.g., validate claim against request header)
const user = await auth.getUser(token, {
customValidator: async (claims) => {
if (claims.organizationId !== requestOrgId) {
throw new Error('Organization mismatch');
}
}
});
console.log(user.sub, user.email, user.name);
Breaking Change Notice: v2.0 introduces major breaking changes including class rename (
Authenticator→Defauth), error class rename (DefAuthError→DefauthError), and private constructor (must useDefauth.create()). See the Migration Guide above for complete upgrade instructions.
const auth = await Defauth.create({
issuer: 'https://your-oidc-provider.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
});
const auth = await Defauth.create({
issuer: 'https://your-oidc-provider.com',
clientId: 'your-public-client-id'
// No client secret needed for public clients like SPAs or mobile apps
});
The Defauth.create() static method is the only recommended way to create a Defauth instance. It returns a Promise that resolves with a fully initialized Defauth:
try {
const auth = await Defauth.create({
issuer: 'https://your-oidc-provider.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
});
// Defauth is guaranteed to be fully initialized and ready to use
const user = await auth.getUser(token);
console.log('User:', user);
} catch (error) {
// Handle initialization failures explicitly
console.error('Failed to initialize authenticator:', error.message);
}
Key benefits of Defauth.create():
getUser() immediately⚠️ Constructor Deprecation: The
new Defauth()constructor is deprecated and will be removed in the next major version. It does not properly initialize the OIDC client and can lead to runtime errors. All code should migrate to usingDefauth.create().
import {
Defauth,
InMemoryStorageAdapter,
ConsoleLogger,
defaultUserInfoRefreshCondition
} from 'defauth';
import type { Logger, LogLevel } from 'defauth';
// Custom logger implementation
class CustomLogger implements Logger {
log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
const timestamp = new Date().toISOString();
const contextStr = context ? ` [Context: ${JSON.stringify(context)}]` : '';
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}${contextStr}`);
}
}
const auth = await Defauth.create({
issuer: 'https://your-oidc-provider.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
// Optional: Custom storage adapter
storageAdapter: new InMemoryStorageAdapter(),
// Optional: Custom logger (defaults to ConsoleLogger)
logger: new CustomLogger(),
// Optional: Throw on UserInfo failure instead of logging warnings (defaults to false)
throwOnUserInfoFailure: true,
// Optional: Disable automatic introspection fallback for failed JWT verification (defaults to false)
disableIntrospectionFallthrough: true,
// Optional: Custom refresh condition
userInfoRefreshCondition: (user, metadata) => {
// Refresh user info every 30 minutes instead of default 1 hour
const thirtyMinutesAgo = new Date(Date.now() - (30 * 60 * 1000));
return !metadata.lastUserInfoRefresh || metadata.lastUserInfoRefresh <= thirtyMinutesAgo;
}
});
The library automatically detects token types and handles them with a hybrid approach:
disableIntrospectionFallthrough)forceIntrospection: trueBy default, when JWT verification fails (e.g., invalid signature, expired token), DefAuth automatically falls back to token introspection. You can disable this behavior:
const auth = await Defauth.create({
issuer: 'https://your-oidc-provider.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
disableIntrospectionFallthrough: true // Throw errors instead of falling back
});
try {
const user = await auth.getUser(jwtToken);
} catch (error) {
if (error instanceof JwtVerificationError) {
// JWT verification failed and no fallback occurred
console.error('JWT is invalid:', error.message);
}
}
When to disable introspection fallback:
Default behavior (recommended):
You can apply custom authentication or authorization logic to validate specific claim values before returning user data. This is useful for multi-tenant applications or request-specific validation:
// Example: Validate organization ID from token matches request header
app.get('/api/resource', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const requestOrgId = req.headers['x-organization-id'];
try {
const user = await auth.getUser(token, {
customValidator: async (claims) => {
// Validate organizationId claim matches request header
if (claims.organizationId !== requestOrgId) {
throw new Error('Token organization does not match request');
}
// Additional validation logic as needed
if (!claims.email_verified) {
throw new Error('Email must be verified');
}
}
});
res.json({ user });
} catch (error) {
if (error instanceof CustomValidationError) {
res.status(403).json({ error: 'Forbidden', message: error.message });
} else {
res.status(401).json({ error: 'Unauthorized' });
}
}
});
Key features:
CustomValidationError when validation failsCommon use cases:
The library supports custom logging implementations for better integration with your application's logging system:
import { Defauth, Logger, LogLevel } from 'defauth';
// Custom logger that integrates with your logging framework
class MyAppLogger implements Logger {
log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
// Integration with your preferred logging library (Winston, Pino, etc.)
myAppLoggingFramework.log({
level,
message,
context,
timestamp: new Date().toISOString(),
service: 'defauth'
});
}
}
const auth = new Defauth({
// ... other config
logger: new MyAppLogger(),
// Control error handling behavior
throwOnUserInfoFailure: false // Log warnings instead of throwing errors
});
You can configure how the library handles UserInfo endpoint failures:
throwOnUserInfoFailure: false (default): Logs warnings and continues with available datathrowOnUserInfoFailure: true: Throws errors when UserInfo endpoint fails// Strict mode - throws on any UserInfo failure
const strictAuth = new Defauth({
// ... config
throwOnUserInfoFailure: true
});
// Resilient mode - logs warnings and continues (default)
const resilientAuth = new Defauth({
// ... config
throwOnUserInfoFailure: false
});
Implement the StorageAdapter interface for your own storage solution:
import { StorageAdapter, StorageMetadata, TokenContext, UserClaims } from 'defauth';
class DatabaseStorageAdapter<TUser = UserClaims> implements StorageAdapter<TUser> {
async findUser(context: TokenContext): Promise<{
user: TUser;
metadata: StorageMetadata;
} | null> {
// Your database lookup logic
const result = await db.users.findOne({ sub: context.sub });
if (!result) return null;
return {
user: result.user,
metadata: result.metadata
};
}
async storeUser(
user: TUser | null,
newClaims: UserClaims,
metadata: StorageMetadata
): Promise<TUser> {
// Create or update user record
const updatedUser = user
? { ...user, ...newClaims } as TUser
: newClaims as unknown as TUser;
// Your database storage logic
await db.users.upsert(
{ sub: newClaims.sub },
{ user: updatedUser, metadata }
);
return updatedUser;
}
}
const auth = new Defauth({
// ... other config
storageAdapter: new DatabaseStorageAdapter()
});
Control when the library should refresh user information:
import { UserInfoRefreshCondition } from 'defauth';
// Never refresh UserInfo (rely only on token/cached data)
const neverRefresh: UserInfoRefreshCondition = () => false;
// Always refresh UserInfo
const alwaysRefresh: UserInfoRefreshCondition = () => true;
// Custom time-based condition
const customCondition: UserInfoRefreshCondition = (user, metadata) => {
if (!metadata.lastUserInfoRefresh) return true;
// Refresh every 15 minutes
const fifteenMinutesAgo = new Date(Date.now() - (15 * 60 * 1000));
return metadata.lastUserInfoRefresh <= fifteenMinutesAgo;
};
const auth = new Defauth({
// ... other config
userInfoRefreshCondition: customCondition
});
static async create<TUser>(config: DefauthConfig<TUser>): Promise<Defauth<TUser>>
Creates and initializes a new Defauth instance. This is the only way to create instances since the constructor is private.
getUser(token: string, options?: JwtValidationOptions): Promise<UserClaims>Main method to extract user information from any token type.
Options:
forceIntrospection?: boolean - Force token introspection even for valid JWTsclockTolerance?: string - Clock tolerance for JWT expiration validation (default: '1 minute')requiredClaims?: string[] - Required claims that must be present in the JWT (default: ['sub', 'exp'])customValidator?: (claims: UserClaims) => Promise<void> | void - Custom validation functionExample:
// Basic usage
const user = await auth.getUser(token);
// With custom validation
const user = await auth.getUser(token, {
customValidator: async (claims) => {
if (claims.organizationId !== requestOrgId) {
throw new Error('Organization mismatch');
}
}
});
// Force introspection
const user = await auth.getUser(token, { forceIntrospection: true });
clearCache(): Promise<void>Clears all cached user data (useful for testing).
UserClaimsStandard OIDC user claims interface.
StorageMetadataMetadata stored alongside user data in storage adapters.
lastUserInfoRefresh?: Date - Timestamp of last UserInfo endpoint refreshlastIntrospection?: Date - Timestamp of last token introspectionUserRecordDeprecated: Extended user record that combines user claims and metadata. Use separate user and metadata objects instead.
DefauthConfigConfiguration object for the authenticator.
StorageAdapter<TUser>Generic interface for implementing custom storage solutions. Methods:
findUser(context: TokenContext): Promise<{user: TUser; metadata: StorageMetadata} | null>storeUser(user: TUser | null, newClaims: UserClaims, metadata: StorageMetadata): Promise<TUser>TokenContextContext object containing token validation information passed to storage adapters.
UserInfoRefreshCondition<TUser>Function type (user: TUser, metadata: StorageMetadata) => boolean for determining when to refresh user information from UserInfo endpoint.
LoggerInterface for implementing custom logging solutions.
LogLevelType for log levels: 'error' | 'warn' | 'info' | 'debug'.
IntrospectionConditionDeprecated alias for UserInfoRefreshCondition.
The library exports Zod schemas for validation:
UserClaimsSchema: Validates user claims (requires only sub field)UserRecordSchema: Validates deprecated user records (includes Date objects for timestamps)IntrospectionResponseSchema: Validates introspection responses from OIDC providersThe library provides structured error handling with custom error classes for different scenarios:
DefAuth exports the following custom error classes:
DefauthError: Base error class for all Defauth errorsInitializationError: Thrown when OIDC client initialization failsTokenValidationError: Thrown when token validation failsJwtVerificationError: Thrown when JWT signature verification fails (extends TokenValidationError)CustomValidationError: Thrown when custom validation fails (extends TokenValidationError)UserInfoError: Thrown when UserInfo endpoint fails (when throwOnUserInfoFailure: true)IntrospectionError: Thrown when token introspection failsimport {
Defauth,
InitializationError,
TokenValidationError,
JwtVerificationError,
CustomValidationError,
UserInfoError,
IntrospectionError
} from 'defauth';
try {
const user = await auth.getUser(token, {
customValidator: async (claims) => {
if (claims.organizationId !== requestOrgId) {
throw new Error('Organization mismatch');
}
}
});
} catch (error) {
if (error instanceof InitializationError) {
// Handle OIDC client initialization failure
console.error('Failed to initialize OIDC client:', error.message);
} else if (error instanceof JwtVerificationError) {
// Handle JWT signature verification failure (when disableIntrospectionFallthrough: true)
console.error('JWT signature verification failed:', error.message);
} else if (error instanceof CustomValidationError) {
// Handle custom validation failure
console.error('Custom validation failed:', error.message);
} else if (error instanceof UserInfoError) {
// Handle UserInfo endpoint failure
console.error('UserInfo fetch failed:', error.message);
} else if (error instanceof IntrospectionError) {
// Handle introspection failure
console.error('Token introspection failed:', error.message);
} else if (error instanceof TokenValidationError) {
// Handle general token validation failure
console.error('Token validation failed:', error.message);
} else {
// Handle other errors
console.error('Unexpected error:', error.message);
}
}
All custom errors preserve the original error as the cause property and include it in the error message for better debugging:
try {
const user = await auth.getUser(token);
} catch (error) {
console.error('Error:', error.message); // Includes cause message
console.error('Original cause:', error.cause); // Access original error
}
MIT
FAQs
A TypeScript OIDC authentication library with support for JWT and opaque tokens
The npm package defauth receives a total of 63 weekly downloads. As such, defauth popularity was classified as not popular.
We found that defauth demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.