
Research
/Security News
Weaponizing Discord for Command and Control Across npm, PyPI, and RubyGems.org
Socket researchers uncover how threat actors weaponize Discord across the npm, PyPI, and RubyGems ecosystems to exfiltrate sensitive data.
@eaccess/auth
Advanced tools
An Express authentication middleware specifically designed for Postgres that provides complete authentication functionality without being tied to any specific ORM, query builder, or user table structure. Comprehensive auth without overwhelming complexity. A clean separation of concerns -- not conflating authentication with user management.
npm install @prsm/easy-auth express-session
import express from 'express';
import session from 'express-session';
import { Pool } from 'pg';
import { createAuthMiddleware, createAuthTables } from '@prsm/easy-auth';
const app = express();
const pool = new Pool({ connectionString: 'postgresql://...' });
// Setup session middleware
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false,
}));
// Configure auth middleware
const authConfig = {
db: pool,
tablePrefix: 'auth_', // Creates: auth_accounts, auth_confirmations, etc.
};
// Create auth tables (run once)
await createAuthTables(authConfig);
// Add auth middleware
app.use(createAuthMiddleware(authConfig));
// Now use auth in your routes
app.post('/register', async (req, res) => {
try {
// Option 1: Let the library auto-generate a UUID for the user
const account = await req.auth.register(
req.body.email,
req.body.password,
undefined, // Auto-generates UUID
(token) => {
// Send confirmation email with token
console.log('Confirmation token:', token);
}
);
// Option 2: Link to your existing user system
// const user = await db.insert(users).values({...}).returning();
// const account = await req.auth.register(
// req.body.email,
// req.body.password,
// user.id, // Link to your user
// (token) => {
// console.log('Confirmation token:', token);
// }
// );
res.json({ success: true, account });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password, req.body.remember);
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: error.message });
}
});
app.get('/profile', (req, res) => {
if (!req.auth.isLoggedIn()) {
return res.status(401).json({ error: 'Not logged in' });
}
res.json({
email: req.auth.getEmail(),
status: req.auth.getStatusName(),
roles: req.auth.getRoleNames(),
isAdmin: await req.auth.isAdmin(),
});
});
Easy-auth supports OAuth providers (GitHub, Google, Azure) with a clean, extensible API.
import express from 'express';
import session from 'express-session';
import { Pool } from 'pg';
import { createAuthMiddleware, createAuthTables, type OAuthUserData } from '@prsm/easy-auth';
const app = express();
const pool = new Pool({ connectionString: 'postgresql://...' });
// Your app's user table (example)
const users: Array<{ id: number; name: string; email: string }> = [];
const authConfig = {
db: pool,
// Optional: OAuth createUser function to handle new user registration
createUser: async (userData: OAuthUserData) => {
// userData contains: { id, email, username?, name?, avatar? }
// Create user in your app's user table
const user = await db.insert(users).values({
name: userData.name || userData.username,
email: userData.email,
}).returning();
return user.id; // Return the new user's ID
},
tablePrefix: 'auth_',
// OAuth provider configuration
providers: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/github/callback'
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/google/callback'
},
azure: {
clientId: process.env.AZURE_CLIENT_ID!,
clientSecret: process.env.AZURE_CLIENT_SECRET!,
tenantId: process.env.AZURE_TENANT_ID!,
redirectUri: 'http://localhost:3000/auth/azure/callback'
}
}
};
app.use(createAuthMiddleware(authConfig));
// Initiate OAuth flow
app.get('/auth/github', (req, res) => {
const authUrl = req.auth.providers.github.getAuthUrl();
res.redirect(authUrl);
});
// Handle OAuth callback (this does everything!)
app.get('/auth/github/callback', async (req, res) => {
try {
await req.auth.providers.github.handleCallback(req);
res.redirect('/dashboard'); // Success!
} catch (error) {
if (error.message.includes('already have an account')) {
res.redirect('/login?error=email_taken');
} else {
res.redirect('/login?error=oauth_failed');
}
}
});
// Same pattern for Google and Azure
app.get('/auth/google', (req, res) => {
const authUrl = req.auth.providers.google.getAuthUrl();
res.redirect(authUrl);
});
app.get('/auth/google/callback', async (req, res) => {
try {
await req.auth.providers.google.handleCallback(req);
res.redirect('/dashboard');
} catch (error) {
res.redirect('/login?error=oauth_failed');
}
});
<!-- Login page -->
<a href="/auth/github" class="oauth-btn">
<img src="/github-icon.svg" /> Login with GitHub
</a>
<a href="/auth/google" class="oauth-btn">
<img src="/google-icon.svg" /> Login with Google
</a>
<a href="/auth/azure" class="oauth-btn">
<img src="/azure-icon.svg" /> Login with Azure
</a>
/auth/github
/auth/github/callback?code=abc123
handleCallback()
does:
createUser()
, create account + provider record, log them inapp.get('/auth/github/callback', async (req, res) => {
try {
await req.auth.providers.github.handleCallback(req);
res.redirect('/dashboard');
} catch (error) {
if (error.message.includes('already have an account')) {
// Email exists with different login method
res.redirect('/login?error=Please use your existing email/password login');
} else if (error.message.includes('No authorization code')) {
// User cancelled or OAuth flow failed
res.redirect('/login?error=Authorization cancelled');
} else {
// Other OAuth errors
console.error('OAuth error:', error);
res.redirect('/login?error=Login failed, please try again');
}
}
});
Create a .env
file:
# GitHub OAuth App (https://github.com/settings/developers)
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Google OAuth App (https://console.cloud.google.com/)
GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Azure OAuth App (https://portal.azure.com/)
AZURE_CLIENT_ID=your_azure_client_id
AZURE_CLIENT_SECRET=your_azure_client_secret
AZURE_TENANT_ID=your_azure_tenant_id
For more control over the OAuth flow:
app.get('/auth/github/callback', async (req, res) => {
try {
// Get user data without logging in
const userData = await req.auth.providers.github.getUserData(req);
// Your custom logic here
const existingUser = await findUserByEmail(userData.email);
if (existingUser && !existingUser.allowOAuth) {
throw new Error('OAuth disabled for this account');
}
// Then complete the OAuth flow manually
await req.auth.providers.github.handleCallback(req);
res.json({ success: true, user: userData });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Easy-auth supports TOTP (authenticator apps), Email OTP, and SMS OTP for enhanced security.
Enable MFA in your auth config:
const authConfig = {
db: pool,
twoFactor: {
enabled: true,
requireForOAuth: false, // Skip MFA for OAuth users (optional)
issuer: 'MyApp', // TOTP issuer name
codeLength: 6, // OTP code length
tokenExpiry: '5m', // OTP expiration
totpWindow: 1, // TOTP time window tolerance
backupCodesCount: 10 // Number of backup codes
}
};
When MFA is enabled, the login process becomes:
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password);
res.json({ success: true });
} catch (error) {
if (error instanceof SecondFactorRequiredError) {
// User needs to complete MFA
return res.status(202).json({
requiresTwoFactor: true,
availableMethods: error.challenge,
message: 'Please complete two-factor authentication'
});
}
res.status(401).json({ error: error.message });
}
});
The SecondFactorRequiredError.challenge
contains:
interface TwoFactorChallenge {
totp?: boolean; // TOTP available
email?: {
otpValue: string; // The actual OTP code that should be sent via email
maskedContact: string; // "j***@example.com"
};
sms?: {
otpValue: string; // The actual OTP code that should be sent via SMS
maskedContact: string; // "+1***90"
};
selectors?: {
email?: string; // Internal selector (stored in session & database)
sms?: string; // Internal selector (stored in session & database)
};
}
Important: The otpValue
fields contain the actual codes that should be delivered to the user. The selectors
are internal identifiers used by the library. In production, you should:
otpValue
codes via your email/SMS serviceotpValue
and selectors
from client responses for securitymaskedContact
to the frontend (selectors are automatically stored in the user's session)After receiving SecondFactorRequiredError
, verify the second factor:
app.post('/verify-2fa', async (req, res) => {
try {
const { code, method } = req.body;
// Verify based on method
switch (method) {
case 'totp':
await req.auth.twoFactor.verify.totp(code);
break;
case 'email':
await req.auth.twoFactor.verify.email(code);
break;
case 'sms':
await req.auth.twoFactor.verify.sms(code);
break;
case 'backup':
await req.auth.twoFactor.verify.backupCode(code);
break;
case 'otp':
// Smart OTP - works for both email and SMS
await req.auth.twoFactor.verify.otp(code);
break;
}
// Complete login
await req.auth.completeTwoFactorLogin();
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Users can enroll in multiple MFA methods:
app.post('/setup-totp', async (req, res) => {
try {
const { secret, qrCode, backupCodes } = await req.auth.twoFactor.setup.totp();
// Show QR code to user for scanning with authenticator app
res.json({
secret, // Manual entry secret
qrCode, // QR code URL for scanning
backupCodes // One-time backup codes
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/setup-email-2fa', async (req, res) => {
try {
await req.auth.twoFactor.setup.email();
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/setup-sms-2fa', async (req, res) => {
try {
const { phoneNumber } = req.body;
await req.auth.twoFactor.setup.sms(phoneNumber);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
For production apps, require verification during enrollment:
app.post('/setup-totp', async (req, res) => {
try {
// Setup but require verification
const { secret, qrCode } = await req.auth.twoFactor.setup.totp(true);
res.json({ secret, qrCode, requiresVerification: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/verify-totp-setup', async (req, res) => {
try {
const { code } = req.body;
const backupCodes = await req.auth.twoFactor.complete.totp(code);
res.json({ success: true, backupCodes });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Check MFA status
app.get('/mfa-status', async (req, res) => {
const status = {
enabled: await req.auth.twoFactor.isEnabled(),
methods: {
totp: await req.auth.twoFactor.totpEnabled(),
email: await req.auth.twoFactor.emailEnabled(),
sms: await req.auth.twoFactor.smsEnabled()
}
};
res.json(status);
});
// Disable MFA method
app.delete('/mfa/:method', async (req, res) => {
try {
const mechanism = req.params.method === 'totp' ? 1 :
req.params.method === 'email' ? 2 : 3;
await req.auth.twoFactor.disable(mechanism);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Generate new backup codes
app.post('/mfa/backup-codes', async (req, res) => {
try {
const backupCodes = await req.auth.twoFactor.generateNewBackupCodes();
res.json({ backupCodes });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
The auth library maintains its own auth tables (accounts, roles, sessions) that can optionally link to your application's user records via a user ID.
Registration now takes an optional userId parameter:
app.post('/register', async (req, res) => {
// Option 1: Let easy-auth auto-generate a UUID (simplest)
const account = await req.auth.register(req.body.email, req.body.password);
// Option 2: Link to your existing user table
const user = await db.insert(users).values({
name: req.body.name,
email: req.body.email
}).returning();
const account = await req.auth.register(req.body.email, req.body.password, user.id);
res.json({ success: true, userId: user.id });
});
For OAuth, you can optionally provide a createUser
function to handle new OAuth users. This is the ONLY use case for createUser
- it's not used for regular registration or admin user creation:
const authConfig = {
db: pool,
// ONLY used for OAuth new user creation
createUser: async (userData: OAuthUserData) => {
// Create user in your app's user table
const user = await db.insert(users).values({
name: userData.name || userData.username,
email: userData.email,
}).returning();
return user.id; // This will be stored as user_id in auth tables
}
}
If you don't provide createUser
for OAuth, a UUID will be auto-generated - no configuration needed!
For login, simply call req.auth.login()
. You don't need to identify the user beforehand because the login
method itself does the authentication using the provided credentials.
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password);
} catch (error) {
if (error instanceof UserNotFoundError || error instanceof InvalidPasswordError) {
return res.status(401).json({ error: 'Invalid email or password' });
}
if (error instanceof UserInactiveError) {
return res.status(403).json({ error: 'Account inactive' });
}
throw error;
}
res.json({ success: true });
});
Important: If you use req.session.userId
, it could be helpful to augment the session type if you're using TypeScript:
declare module "express-session" {
interface SessionData {
userId?: string;
}
}
interface AuthConfig {
// PostgreSQL connection pool
db: Pool;
// Optional OAuth new user creation function
createUser?: (userData: OAuthUserData) => string | number | Promise<string | number>; // Called when OAuth user doesn't exist in your system
// Optional settings
tablePrefix?: string; // default: 'user_'
minPasswordLength?: number; // default: 8
maxPasswordLength?: number; // default: 64
rememberDuration?: string; // default: '30d'
rememberCookieName?: string; // default: 'remember_token'
resyncInterval?: string; // default: '30s'
// OAuth provider configuration
providers?: {
github?: GitHubProviderConfig;
google?: GoogleProviderConfig;
azure?: AzureProviderConfig;
};
// Multi-factor authentication
twoFactor?: {
enabled?: boolean; // default: false
requireForOAuth?: boolean; // default: false
issuer?: string; // default: 'EasyAuth'
codeLength?: number; // default: 6
tokenExpiry?: string; // default: '5m'
totpWindow?: number; // default: 1
backupCodesCount?: number; // default: 10
};
}
The library creates its own tables that link to your existing user table:
-- your existing user table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
-- whatever else
);
-- library creates these tables
CREATE TABLE user_accounts (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL, -- links to your users.id or auto-generated UUID
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
verified BOOLEAN DEFAULT FALSE,
status INTEGER DEFAULT 0,
rolemask INTEGER DEFAULT 0,
-- ...
);
-- also: user_confirmations, user_remembers, user_resets, user_providers
-- MFA tables: user_2fa_methods, user_2fa_tokens
-- Activity: user_activity_log
req.auth
)isLoggedIn(): boolean
login(email, password, remember?): Promise<void>
completeTwoFactorLogin(): Promise<void>
logout(): Promise<void>
register(email, password, callback?): Promise<AuthAccount>
getId(): number | null
getEmail(): string | null
getStatus(): number | null
getVerified(): boolean | null
getRoleNames(rolemask?): string[]
getStatusName(): string | null
hasRole(role): Promise<boolean>
isAdmin(): Promise<boolean>
isRemembered(): boolean
changeEmail(newEmail, callback): Promise<void>
confirmEmail(token): Promise<string>
confirmEmailAndLogin(token, remember?): Promise<void>
resetPassword(email, expiresAfter?, maxRequests?, callback?): Promise<void>
confirmResetPassword(token, password, logout?): Promise<void>
verifyPassword(password): Promise<boolean>
logoutEverywhere(): Promise<void>
logoutEverywhereElse(): Promise<void>
req.auth.twoFactor
)isEnabled(): Promise<boolean>
totpEnabled(): Promise<boolean>
emailEnabled(): Promise<boolean>
smsEnabled(): Promise<boolean>
getEnabledMethods(): Promise<TwoFactorMechanism[]>
Setup Methods:
setup.totp(requireVerification?): Promise<TwoFactorSetupResult>
setup.email(email?, requireVerification?): Promise<void>
setup.sms(phone, requireVerification?): Promise<void>
Completion Methods (for verification during enrollment):
complete.totp(code): Promise<string[]>
complete.email(code): Promise<void>
complete.sms(code): Promise<void>
Verification Methods (during login):
verify.totp(code): Promise<void>
verify.email(code): Promise<void>
verify.sms(code): Promise<void>
verify.backupCode(code): Promise<void>
verify.otp(code): Promise<void>
Management Methods:
disable(mechanism): Promise<void>
generateNewBackupCodes(): Promise<string[]>
getContact(mechanism): Promise<string | null>
req.authAdmin
)createUser(credentials, callback?): Promise<AuthAccount>
loginAsUserBy(identifier): Promise<void>
deleteUserBy(identifier): Promise<void>
addRoleForUserBy(identifier, role): Promise<void>
removeRoleForUserBy(identifier, role): Promise<void>
hasRoleForUserBy(identifier, role): Promise<boolean>
changePasswordForUserBy(identifier, password): Promise<void>
setStatusForUserBy(identifier, status): Promise<void>
initiatePasswordResetForUserBy(identifier, expiresAfter?, callback?): Promise<void>
import { createAuthTables, dropAuthTables, cleanupExpiredTokens, getAuthTableStats } from '@prsm/easy-auth';
// Setup tables
await createAuthTables(config);
// Cleanup (useful for cron jobs)
await cleanupExpiredTokens(config);
// Get statistics
const stats = await getAuthTableStats(config);
console.log(`${stats.accounts} accounts, ${stats.expiredRemembers} expired tokens`);
// Remove all auth tables
await dropAuthTables(config);
import { AuthStatus, AuthRole } from '@prsm/easy-auth';
// User statuses
AuthStatus.Normal // 0
AuthStatus.Archived // 1
AuthStatus.Banned // 2
AuthStatus.Locked // 3
AuthStatus.PendingReview // 4
AuthStatus.Suspended // 5
// User roles (bitmask)
AuthRole.Admin // 1
AuthRole.Author // 2
AuthRole.Collaborator // 4
// ... many more
import {
EmailTakenError,
InvalidPasswordError,
UserNotFoundError,
SecondFactorRequiredError,
InvalidTwoFactorCodeError
} from '@prsm/easy-auth';
app.post('/register', async (req, res) => {
try {
await req.auth.register(email, password);
} catch (error) {
if (error instanceof EmailTakenError) {
return res.status(409).json({ error: 'Email already exists' });
}
if (error instanceof InvalidPasswordError) {
return res.status(400).json({ error: 'Password too weak' });
}
throw error;
}
});
app.post('/login', async (req, res) => {
try {
await req.auth.login(req.body.email, req.body.password);
res.json({ success: true });
} catch (error) {
if (error instanceof SecondFactorRequiredError) {
return res.status(202).json({
requiresTwoFactor: true,
availableMethods: error.challenge
});
}
if (error instanceof InvalidTwoFactorCodeError) {
return res.status(400).json({ error: 'Invalid verification code' });
}
throw error;
}
});
import { Pool } from 'pg';
const pool = new Pool({
connectionString: 'postgresql://user:password@localhost:5432/dbname'
});
const config = {
db: pool,
tablePrefix: 'auth_',
};
app.get('/admin', async (req, res) => {
if (!req.auth.isLoggedIn()) {
return res.status(401).json({ error: 'Not logged in' });
}
if (!await req.auth.hasRole(AuthRole.Admin)) {
return res.status(403).json({ error: 'Admin access required' });
}
// Admin-only content
});
// Add role to user
await req.authAdmin.addRoleForUserBy(
{ email: 'user@example.com' },
AuthRole.Admin | AuthRole.Editor
);
MIT
FAQs
Database-agnostic Express authentication middleware for PostgreSQL
The npm package @eaccess/auth receives a total of 183 weekly downloads. As such, @eaccess/auth popularity was classified as not popular.
We found that @eaccess/auth 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.
Research
/Security News
Socket researchers uncover how threat actors weaponize Discord across the npm, PyPI, and RubyGems ecosystems to exfiltrate sensitive data.
Security News
Socket now integrates with Bun 1.3’s Security Scanner API to block risky packages at install time and enforce your organization’s policies in local dev and CI.
Research
The Socket Threat Research Team is tracking weekly intrusions into the npm registry that follow a repeatable adversarial playbook used by North Korean state-sponsored actors.