@winwinmbs/portal-auth-server
v1.0.0
Published
Server-side auth for WINWIN Portal — Express middleware + NestJS guard
Readme
@winwinmbs/portal-auth-server
Server-side auth for WINWIN Portal — Express middleware + NestJS guards, plus an OAuth token-introspection validator (RFC 7662).
One package, three entry points:
| Import path | Contents |
|---|---|
| @winwinmbs/portal-auth-server | Shared types, AuthService, token extractor, user cache |
| @winwinmbs/portal-auth-server/express | authMiddleware, oauthMiddleware, getAuth, requireAuth, getOAuth, requireOAuth |
| @winwinmbs/portal-auth-server/nestjs | createAuthGuard, createOptionalAuthGuard, createOAuthGuard, createOptionalOAuthGuard |
Companion to @winwinmbs/portal-auth (browser-side SSO client).
Install
Consumers only need the peer for the framework they use. Both @nestjs/common and express are declared as optional peer dependencies.
# Express-only consumer
npm install @winwinmbs/portal-auth-server express
# NestJS-only consumer
npm install @winwinmbs/portal-auth-server @nestjs/common reflect-metadataExpress usage
Validate portal-issued JWTs against /auth/me on every request and attach req.user (a UserProfileResponseDto).
import express from 'express';
import { authMiddleware, requireAuth } from '@winwinmbs/portal-auth-server/express';
const app = express();
app.use(authMiddleware({
baseURL: process.env.PORTAL_API_URL!,
apiKey: process.env.PORTAL_API_KEY!,
cacheTimeout: 300, // seconds — default 5 min
excludePaths: [/^\/public\//, '/health'],
}));
app.get('/me', (req, res) => {
const { user } = requireAuth(req); // throws if unauthenticated
res.json(user);
});OAuth bearer-token validation (for third-party services consuming portal-issued access tokens):
import { oauthMiddleware, requireOAuth } from '@winwinmbs/portal-auth-server/express';
app.use('/api', oauthMiddleware({
issuerUrl: process.env.PORTAL_URL!,
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
requiredScopes: ['profile.read'],
cacheTimeout: 60,
}));
app.get('/api/data', (req, res) => {
const { oauthUser } = requireOAuth(req);
res.json({ userId: oauthUser.sub, scopes: oauthUser.scope });
});NestJS usage
// auth/auth.guard.ts
import { createAuthGuard } from '@winwinmbs/portal-auth-server/nestjs';
export const PortalAuthGuard = createAuthGuard({
baseURL: process.env.PORTAL_API_URL!,
apiKey: process.env.PORTAL_API_KEY!,
});
// auth/current-user.decorator.ts — consumer defines the decorator locally
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { UserProfileResponseDto } from '@win-portal/shared/auth';
export const CurrentUser = createParamDecorator(
(_: unknown, ctx: ExecutionContext): UserProfileResponseDto =>
ctx.switchToHttp().getRequest().user,
);
// in a controller
@UseGuards(PortalAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: UserProfileResponseDto) {
return user;
}OAuth guard:
import { createOAuthGuard } from '@winwinmbs/portal-auth-server/nestjs';
export const OAuthGuard = createOAuthGuard({
issuerUrl: process.env.PORTAL_URL!,
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
});How verification works
- Token is extracted from the request (
Authorization: Bearer ...by default — configurable viatokenStrategy: 'cookie' | 'custom') - In-memory cache is consulted (keyed by token, TTL =
cacheTimeout) - On cache miss:
- Session tokens →
GET /auth/meon the portal API with the token + API key, unwrapsApiResponse<UserProfileResponseDto> - OAuth bearer tokens → OIDC discovery + token introspection (RFC 7662), with optional UserInfo fetch
- Session tokens →
- Result is cached and attached to the request (
req.user/req.oauthUser)
Cache is per-process. Multi-instance deployments share nothing — that is by design; the TTL is short and portal /auth/me is fast.
Options
Both MiddlewareConfig and OAuthMiddlewareConfig support:
tokenStrategy: 'bearer' | 'cookie' | 'custom'(defaultbearer)cookieName(defaultaccess_token)tokenExtractor: (req) => string | nullexcludePaths: (string | RegExp)[]optional: boolean— when true, an unauthenticated request passes through withuser: nullinstead of a 401
See src/types.ts and src/oauth-types.ts for full shapes.
License
MIT
