Package Exports
- @sp-uvb/koa
Readme
@sp-uvb/koa
Koa middleware for Universal Verification Broker (UVB) authentication.
Installation
npm install @sp-uvb/koa
# or
yarn add @sp-uvb/koa
# or
pnpm add @sp-uvb/koaQuick Start
import Koa from 'koa';
import Router from '@koa/router';
import { uvbMiddleware, requireFactors } from '@sp-uvb/koa';
const app = new Koa();
const router = new Router();
// Apply UVB authentication to all routes
app.use(
uvbMiddleware({
tenantId: 'my-tenant',
uvbUrl: 'http://localhost:8080',
excludePaths: ['/login', '/health', '/public'],
})
);
// Access the authenticated user
router.get('/profile', async (ctx) => {
const session = ctx.uvbSession;
ctx.body = {
userId: session?.userId,
factors: session?.factorsVerified,
};
});
// Require specific MFA factors for sensitive routes
router.post('/admin/delete', requireFactors(['totp', 'webauthn']), async (ctx) => {
ctx.body = { message: 'Admin action completed' };
});
app.use(router.routes());
app.listen(3000);API
uvbMiddleware(options)
Main authentication middleware.
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_sessionexcludePaths(optional): Array of paths to exclude from authentication, defaults to[]onError(optional): Custom error handler function
Attached to Context:
After successful authentication, ctx.uvbSession contains:
interface UVBSession {
userId: string;
tenantId: string;
factorsVerified: string[];
sessionId: string;
expiresAt: Date;
}requireFactors(factors: string[])
Middleware to require specific MFA factors for a route.
router.get('/sensitive', requireFactors(['totp', 'webauthn']), async (ctx) => {
// Only accessible if user has verified both TOTP and WebAuthn
ctx.body = { message: 'Sensitive data' };
});getSession(ctx: Context)
Helper function to get the current UVB session from a context.
import { getSession } from '@sp-uvb/koa';
router.get('/info', async (ctx) => {
const session = getSession(ctx);
if (!session) {
ctx.status = 401;
ctx.body = { error: 'Not authenticated' };
return;
}
ctx.body = { userId: session.userId };
});Examples
Protecting Specific Routes
import Koa from 'koa';
import Router from '@koa/router';
import { uvbMiddleware } from '@sp-uvb/koa';
const app = new Koa();
const publicRouter = new Router();
const protectedRouter = new Router();
// Public routes
publicRouter.get('/health', async (ctx) => {
ctx.body = { status: 'ok' };
});
// Protected routes
protectedRouter.use(
uvbMiddleware({
tenantId: 'my-tenant',
uvbUrl: process.env.UVB_URL,
})
);
protectedRouter.get('/api/data', async (ctx) => {
// User is authenticated
ctx.body = { data: 'protected' };
});
app.use(publicRouter.routes());
app.use(protectedRouter.routes());
app.listen(3000);Custom Error Handling
app.use(
uvbMiddleware({
tenantId: 'my-tenant',
onError: (err, ctx) => {
console.error('UVB auth error:', err);
ctx.status = 401;
ctx.body = {
error: 'Authentication failed',
details: err.message,
};
},
})
);Conditional MFA Requirements
router.post(
'/transfer',
async (ctx, next) => {
const amount = ctx.request.body.amount;
// Require additional auth for large transfers
if (amount > 10000) {
await requireFactors(['totp', 'webauthn'])(ctx, next);
} else {
await next();
}
},
async (ctx) => {
ctx.body = { message: 'Transfer initiated' };
}
);Combining Multiple Middlewares
import compose from 'koa-compose';
import { uvbMiddleware, requireFactors } from '@sp-uvb/koa';
// Create a composed middleware for admin routes
const adminAuth = compose([
uvbMiddleware({ tenantId: 'my-tenant' }),
requireFactors(['totp', 'webauthn']),
]);
router.delete('/admin/user/:id', adminAuth, async (ctx) => {
ctx.body = { message: 'User deleted' };
});Per-Route Authentication
import Koa from 'koa';
import Router from '@koa/router';
import { uvbMiddleware } from '@sp-uvb/koa';
const app = new Koa();
const router = new Router();
// Create auth middleware
const auth = uvbMiddleware({ tenantId: 'my-tenant' });
// Apply selectively to routes
router.get('/public', async (ctx) => {
ctx.body = { message: 'Public data' };
});
router.get('/private', auth, async (ctx) => {
ctx.body = {
message: 'Private data',
userId: ctx.uvbSession?.userId,
};
});
app.use(router.routes());
app.listen(3000);TypeScript
This package includes TypeScript definitions. The Context type is automatically extended:
import { Context } from 'koa';
router.get('/test', async (ctx: Context) => {
// TypeScript knows about uvbSession
const userId = ctx.uvbSession?.userId;
ctx.body = { userId };
});License
MIT