Package Exports
- @sp-uvb/feathers
- @sp-uvb/feathers/vue
Readme
@sp-uvb/feathers
FeathersJS hooks and services for Universal Verification Broker (UVB) authentication.
Installation
npm install @sp-uvb/feathers
# or
yarn add @sp-uvb/feathers
# or
pnpm add @sp-uvb/feathersQuick Start
import { feathers } from '@feathersjs/feathers';
import { authenticate, requireFactors } from '@sp-uvb/feathers';
const app = feathers();
// Apply authentication to all services
app.hooks({
before: {
all: [authenticate({ tenantId: 'my-tenant' })],
},
});
// Require specific factors for sensitive operations
app.service('admin').hooks({
before: {
all: [requireFactors(['totp', 'webauthn'])],
},
});
// Access session in service methods
app.service('users').hooks({
before: {
find: [
async (context) => {
const session = context.params.uvbSession;
console.log('User ID:', session?.userId);
return context;
},
],
},
});API
authenticate(options)
Feathers hook to authenticate requests with UVB.
Options:
tenantId(required): Your UVB tenant IDuvbUrl(optional): UVB server URL, defaults tohttp://localhost:8080apiKey(optional): API key for server-to-server authenticationcookieName(optional): Cookie name for session token, defaults touvb_sessionexcludeServices(optional): Array of service paths to exclude from authentication
import { authenticate } from '@sp-uvb/feathers';
// Global authentication
app.hooks({
before: {
all: [
authenticate({
tenantId: 'my-tenant',
uvbUrl: 'http://localhost:8080',
excludeServices: ['authentication', 'health'],
}),
],
},
});
// Per-service authentication
app.service('users').hooks({
before: {
all: [authenticate({ tenantId: 'my-tenant' })],
},
});Session Access:
After authentication, context.params.uvbSession contains:
interface UvbSession {
userId: string;
tenantId: string;
sessionId: string;
factorsVerified: string[];
expiresAt: Date;
}requireFactors(factors)
Hook to require specific MFA factors.
import { authenticate, requireFactors } from '@sp-uvb/feathers';
app.service('admin').hooks({
before: {
all: [authenticate({ tenantId: 'my-tenant' }), requireFactors(['totp', 'webauthn'])],
},
});requireOwnership(ownerField)
Hook to require user owns the resource.
import { authenticate, requireOwnership } from '@sp-uvb/feathers';
app.service('posts').hooks({
before: {
update: [
authenticate({ tenantId: 'my-tenant' }),
requireOwnership('userId'), // Checks if record.userId === session.userId
],
remove: [authenticate({ tenantId: 'my-tenant' }), requireOwnership('userId')],
create: [
authenticate({ tenantId: 'my-tenant' }),
requireOwnership('userId'), // Automatically sets record.userId = session.userId
],
},
});getSession(context)
Helper to get UVB session from hook context.
import { getSession } from '@sp-uvb/feathers';
app.service('users').hooks({
before: {
find: [
async (context) => {
const session = getSession(context);
if (session) {
context.params.query = {
...context.params.query,
userId: session.userId,
};
}
return context;
},
],
},
});UvbService
Feathers service for validating and revoking UVB sessions.
import { UvbService } from '@sp-uvb/feathers';
// Register service
app.use(
'uvb',
new UvbService({
tenantId: 'my-tenant',
uvbUrl: 'http://localhost:8080',
})
);
// Validate session
const session = await app.service('uvb').get(sessionToken);
console.log('User ID:', session.userId);
// Revoke session
await app.service('uvb').remove(sessionId);Examples
Protecting All Services Except Authentication
import { authenticate } from '@sp-uvb/feathers';
app.hooks({
before: {
all: [
authenticate({
tenantId: 'my-tenant',
excludeServices: ['authentication', 'health', 'public'],
}),
],
},
});Per-Method Authentication Requirements
import { authenticate, requireFactors } from '@sp-uvb/feathers';
app.service('users').hooks({
before: {
find: [authenticate({ tenantId: 'my-tenant' })],
get: [authenticate({ tenantId: 'my-tenant' })],
create: [authenticate({ tenantId: 'my-tenant' })],
update: [authenticate({ tenantId: 'my-tenant' }), requireFactors(['totp'])],
remove: [authenticate({ tenantId: 'my-tenant' }), requireFactors(['totp', 'webauthn'])],
},
});Filtering Results by User
import { authenticate, getSession } from '@sp-uvb/feathers';
app.service('posts').hooks({
before: {
find: [
authenticate({ tenantId: 'my-tenant' }),
async (context) => {
const session = getSession(context);
// Only show user's own posts
context.params.query = {
...context.params.query,
userId: session.userId,
};
return context;
},
],
},
});Conditional MFA Requirements
import { authenticate, requireFactors, getSession } from '@sp-uvb/feathers';
app.service('transfers').hooks({
before: {
create: [
authenticate({ tenantId: 'my-tenant' }),
async (context) => {
const amount = context.data.amount;
// Require strong auth for large transfers
if (amount > 10000) {
await requireFactors(['totp', 'webauthn'])(context);
}
return context;
},
],
},
});Admin-Only Operations
import { authenticate, getSession } from '@sp-uvb/feathers';
const requireAdmin = async (context) => {
const session = getSession(context);
if (!session) {
throw new Error('No session found');
}
// Check if user has admin factor
if (!session.factorsVerified.includes('admin-role')) {
const error = new Error('Admin access required') as any;
error.code = 403;
throw error;
}
return context;
};
app.service('admin').hooks({
before: {
all: [authenticate({ tenantId: 'my-tenant' }), requireAdmin],
},
});Audit Logging with Session
import { authenticate, getSession } from '@sp-uvb/feathers';
app.service('logs').hooks({
after: {
create: [
async (context) => {
const session = getSession(context);
await app.service('audit-logs').create({
action: 'log_created',
userId: session?.userId,
timestamp: new Date(),
data: context.result,
});
return context;
},
],
},
});Using with Feathers Authentication
import { authenticate as feathersAuth } from '@feathersjs/authentication';
import { authenticate as uvbAuth } from '@sp-uvb/feathers';
// Use Feathers auth for local auth endpoints
app.service('authentication').hooks({
before: {
create: [feathersAuth('local')],
},
});
// Use UVB auth for everything else
app.hooks({
before: {
all: [
uvbAuth({
tenantId: 'my-tenant',
excludeServices: ['authentication'],
}),
],
},
});Combining Multiple Security Hooks
import { authenticate, requireFactors, requireOwnership } from '@sp-uvb/feathers';
app.service('private-docs').hooks({
before: {
all: [authenticate({ tenantId: 'my-tenant' })],
find: [],
get: [requireOwnership('userId')],
create: [requireFactors(['email-verified']), requireOwnership('userId')],
update: [requireFactors(['totp']), requireOwnership('userId')],
remove: [requireFactors(['totp', 'webauthn']), requireOwnership('userId')],
},
});Custom Session Validation
import { UvbService } from '@sp-uvb/feathers';
// Register UVB service
app.use('uvb', new UvbService({ tenantId: 'my-tenant' }));
// Custom authentication hook
const customAuth = async (context) => {
const token = context.params.headers?.['x-custom-token'];
if (!token) {
throw new Error('No token provided');
}
try {
const session = await app.service('uvb').get(token);
context.params.uvbSession = session;
return context;
} catch (error) {
throw new Error('Invalid token');
}
};
app.service('custom').hooks({
before: {
all: [customAuth],
},
});TypeScript
This package includes full TypeScript definitions:
import type { UvbSession, UvbAuthenticateOptions } from '@sp-uvb/feathers';
// Extend Feathers Params type (done automatically by the package)
declare module '@feathersjs/feathers' {
interface Params {
uvbSession?: UvbSession;
}
}License
MIT