First commit
Some checks failed
armco-org/iam-server-sdk/pipeline/head There was a failure building this commit

This commit is contained in:
2025-12-28 19:00:00 +05:30
commit eeebebd2dc
10 changed files with 719 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

7
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,7 @@
@Library('jenkins-shared') _
kanikoPipeline(
repoName: 'iam-server-sdk',
branch: env.BRANCH_NAME ?: 'main',
isNpmLib: true
)

143
README.md Normal file
View File

@@ -0,0 +1,143 @@
# @armco/iam-server
Server-side JWT validation and middleware for IAM.
## Installation
```bash
npm install @armco/iam-server
```
## Quick Start
### Standalone Verifier
```typescript
import { createIAMVerifier } from '@armco/iam-server';
const verifier = createIAMVerifier({
issuer: 'http://localhost:5000',
audience: 'my-api',
});
// Verify a token
const result = await verifier.verify(token);
if (result.valid) {
console.log('User ID:', result.payload.sub);
console.log('Email:', result.payload.email);
console.log('Roles:', result.payload.roles);
}
// Or authenticate and get structured user info
const user = await verifier.authenticate(token);
if (user) {
console.log(user.id, user.email, user.roles, user.scopes);
}
```
### Express Middleware
```typescript
import express from 'express';
import { createAuthMiddleware, requireRole } from '@armco/iam-server/express';
const app = express();
// Create auth middleware
const auth = createAuthMiddleware({
issuer: 'http://localhost:5000',
audience: 'my-api',
});
// Protect all /api routes
app.use('/api', auth());
// Access user in handlers
app.get('/api/profile', (req, res) => {
res.json({
id: req.user.id,
email: req.user.email,
roles: req.user.roles,
});
});
// Require specific role
app.get('/api/admin', auth({ roles: ['admin'] }), (req, res) => {
res.json({ message: 'Admin access granted' });
});
// Require specific scope
app.get('/api/data', auth({ scopes: ['read:data'] }), (req, res) => {
res.json({ data: '...' });
});
// Or use standalone middleware
app.delete('/api/users/:id', auth(), requireRole('admin'), handler);
```
## Configuration
```typescript
interface IAMServerConfig {
/** IAM server base URL */
issuer: string;
/** Expected audience (your app's client_id) */
audience: string;
/** Cache JWKS keys (default: true) */
cacheKeys?: boolean;
/** JWKS cache TTL in seconds (default: 3600) */
cacheTTL?: number;
/** Required scopes for all requests */
requiredScopes?: string[];
/** Custom claim to extract user ID from (default: 'sub') */
userIdClaim?: string;
}
```
## API Reference
### IAMVerifier
| Method | Description |
|--------|-------------|
| `verify(token)` | Verify JWT, returns `{ valid, payload?, error? }` |
| `authenticate(token)` | Verify and return `AuthenticatedUser` or `null` |
| `hasRole(user, roles)` | Check if user has any of the roles |
| `hasAllRoles(user, roles)` | Check if user has all roles |
| `hasScope(user, scopes)` | Check if user has any of the scopes |
| `hasAllScopes(user, scopes)` | Check if user has all scopes |
| `clearCache()` | Force JWKS refresh on next verify |
### Express Middleware
| Function | Description |
|----------|-------------|
| `createAuthMiddleware(config)` | Create auth middleware factory |
| `auth(options?)` | Middleware that requires valid token |
| `requireRole(roles)` | Middleware to check roles (use after auth) |
| `requireScope(scopes)` | Middleware to check scopes (use after auth) |
| `createOptionalAuthMiddleware(config)` | Attaches user if token present, doesn't require it |
### AuthenticatedUser
```typescript
interface AuthenticatedUser {
id: string; // Global identity ID (from 'sub')
userId?: string; // Tenant-specific user ID
tenantId?: string; // Tenant ID
email?: string;
username?: string;
roles: string[];
scopes: string[];
claims: JWTPayload; // Raw JWT payload
}
```
## Development
```bash
cd packages/iam-server
npm install
npm run build
npm run dev # Watch mode
```

60
package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "@armco/iam-server",
"version": "0.1.0",
"description": "Server-side JWT validation and middleware for IAM",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./express": {
"import": "./dist/express.mjs",
"require": "./dist/express.js",
"types": "./dist/express.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"keywords": [
"iam",
"jwt",
"authentication",
"middleware",
"armco",
"express"
],
"author": "Armco",
"license": "MIT",
"peerDependencies": {
"express": ">=4.0.0"
},
"peerDependenciesMeta": {
"express": {
"optional": true
}
},
"dependencies": {
"jose": "^5.2.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"express": "^4.18.2",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
}
}

200
src/express/index.ts Normal file
View File

@@ -0,0 +1,200 @@
/**
* Express middleware for IAM authentication
*/
import type { Request, Response, NextFunction } from 'express';
import { IAMVerifier, createIAMVerifier } from '../verifier';
import type { IAMServerConfig, AuthenticatedUser, AuthOptions } from '../types';
// Extend Express Request to include user
declare global {
namespace Express {
interface Request {
user?: AuthenticatedUser;
}
}
}
/**
* Extract Bearer token from Authorization header
*/
function extractToken(req: Request): string | null {
const authHeader = req.headers.authorization;
if (!authHeader) return null;
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
return null;
}
return parts[1];
}
/**
* Create authentication middleware
*
* @example
* ```ts
* import { createAuthMiddleware } from '@armco/iam-server/express';
*
* const auth = createAuthMiddleware({
* issuer: 'http://localhost:5000',
* audience: 'my-api',
* });
*
* // Protect all routes
* app.use('/api', auth());
*
* // Or specific routes
* app.get('/api/users', auth(), (req, res) => {
* console.log(req.user); // AuthenticatedUser
* });
* ```
*/
export function createAuthMiddleware(config: IAMServerConfig) {
const verifier = createIAMVerifier(config);
/**
* Authentication middleware factory
*/
return function auth(options: AuthOptions = {}) {
return async (req: Request, res: Response, next: NextFunction) => {
const token = extractToken(req);
if (!token) {
return res.status(401).json({
error: 'unauthorized',
message: 'Missing or invalid Authorization header',
});
}
const user = await verifier.authenticate(token);
if (!user) {
return res.status(401).json({
error: 'unauthorized',
message: 'Invalid or expired token',
});
}
// Check required scopes
if (options.scopes && options.scopes.length > 0) {
const hasScope = options.requireAllScopes
? verifier.hasAllScopes(user, options.scopes)
: verifier.hasScope(user, options.scopes);
if (!hasScope) {
return res.status(403).json({
error: 'forbidden',
message: `Insufficient scope. Required: ${options.scopes.join(', ')}`,
});
}
}
// Check required roles
if (options.roles && options.roles.length > 0) {
const hasRole = options.requireAllRoles
? verifier.hasAllRoles(user, options.roles)
: verifier.hasRole(user, options.roles);
if (!hasRole) {
return res.status(403).json({
error: 'forbidden',
message: `Insufficient permissions. Required role: ${options.roles.join(' or ')}`,
});
}
}
// Attach user to request
req.user = user;
next();
};
};
}
/**
* Create role-checking middleware (use after auth middleware)
*
* @example
* ```ts
* app.get('/admin', auth(), requireRole('admin'), handler);
* app.get('/editor', auth(), requireRole(['admin', 'editor']), handler);
* ```
*/
export function requireRole(roles: string | string[]) {
const required = Array.isArray(roles) ? roles : [roles];
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
error: 'unauthorized',
message: 'Authentication required',
});
}
const hasRole = required.some(role => req.user!.roles.includes(role));
if (!hasRole) {
return res.status(403).json({
error: 'forbidden',
message: `Required role: ${required.join(' or ')}`,
});
}
next();
};
}
/**
* Create scope-checking middleware (use after auth middleware)
*
* @example
* ```ts
* app.get('/data', auth(), requireScope('read:data'), handler);
* ```
*/
export function requireScope(scopes: string | string[]) {
const required = Array.isArray(scopes) ? scopes : [scopes];
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
error: 'unauthorized',
message: 'Authentication required',
});
}
const hasScope = required.some(scope => req.user!.scopes.includes(scope));
if (!hasScope) {
return res.status(403).json({
error: 'forbidden',
message: `Required scope: ${required.join(' or ')}`,
});
}
next();
};
}
/**
* Optional auth - attaches user if token present, but doesn't require it
*/
export function createOptionalAuthMiddleware(config: IAMServerConfig) {
const verifier = createIAMVerifier(config);
return async (req: Request, res: Response, next: NextFunction) => {
const token = extractToken(req);
if (token) {
const user = await verifier.authenticate(token);
if (user) {
req.user = user;
}
}
next();
};
}
// Re-export types
export type { IAMServerConfig, AuthenticatedUser, AuthOptions } from '../types';
export { IAMVerifier, createIAMVerifier } from '../verifier';

41
src/index.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* @armco/iam-server
*
* Server-side JWT validation and utilities for IAM.
*
* @example
* ```ts
* import { createIAMVerifier } from '@armco/iam-server';
*
* const verifier = createIAMVerifier({
* issuer: 'http://localhost:5000',
* audience: 'my-api',
* });
*
* // Verify a token
* const result = await verifier.verify(token);
* if (result.valid) {
* console.log(result.payload);
* }
*
* // Or authenticate and get user info
* const user = await verifier.authenticate(token);
* if (user) {
* console.log(user.email, user.roles);
* }
* ```
*
* For Express middleware, use:
* ```ts
* import { createAuthMiddleware } from '@armco/iam-server/express';
* ```
*/
export { IAMVerifier, createIAMVerifier } from './verifier';
export type {
IAMServerConfig,
JWTPayload,
VerifyResult,
AuthenticatedUser,
AuthOptions,
} from './types';

83
src/types.ts Normal file
View File

@@ -0,0 +1,83 @@
/**
* @armco/iam-server Types
*/
export interface IAMServerConfig {
/** IAM server base URL (e.g., http://localhost:5000) */
issuer: string;
/** Expected audience (your app's client_id) */
audience: string;
/** Cache JWKS keys (default: true) */
cacheKeys?: boolean;
/** JWKS cache TTL in seconds (default: 3600) */
cacheTTL?: number;
/** Required scopes for all requests (optional) */
requiredScopes?: string[];
/** Custom claim to extract user ID from (default: 'sub') */
userIdClaim?: string;
}
export interface JWTPayload {
/** Subject - global identity ID */
sub: string;
/** Issuer */
iss: string;
/** Audience */
aud: string | string[];
/** Issued at */
iat: number;
/** Expiration */
exp: number;
/** User's email */
email?: string;
/** Email verified */
email_verified?: boolean;
/** Username */
username?: string;
/** Tenant ID */
tenantId?: string;
/** User ID (membership ID) */
userId?: string;
/** Roles */
roles?: string[];
/** Scopes */
scope?: string;
/** Additional claims */
[key: string]: unknown;
}
export interface VerifyResult {
valid: boolean;
payload?: JWTPayload;
error?: string;
}
export interface AuthenticatedUser {
/** Global identity ID (from 'sub' claim) */
id: string;
/** Tenant-specific user ID */
userId?: string;
/** Tenant ID */
tenantId?: string;
/** Email */
email?: string;
/** Username */
username?: string;
/** Roles */
roles: string[];
/** Scopes */
scopes: string[];
/** Raw JWT payload */
claims: JWTPayload;
}
export interface AuthOptions {
/** Required scopes (any of these) */
scopes?: string[];
/** Required roles (any of these) */
roles?: string[];
/** Require all specified scopes (default: false - any match) */
requireAllScopes?: boolean;
/** Require all specified roles (default: false - any match) */
requireAllRoles?: boolean;
}

154
src/verifier.ts Normal file
View File

@@ -0,0 +1,154 @@
/**
* JWT Verifier using jose library
* Fetches JWKS from IAM and validates tokens
*/
import { createRemoteJWKSet, jwtVerify, type JWTVerifyResult } from 'jose';
import type { IAMServerConfig, JWTPayload, VerifyResult, AuthenticatedUser } from './types';
export class IAMVerifier {
private config: Required<IAMServerConfig>;
private jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
private jwksCreatedAt: number = 0;
constructor(config: IAMServerConfig) {
this.config = {
issuer: config.issuer.replace(/\/$/, ''),
audience: config.audience,
cacheKeys: config.cacheKeys ?? true,
cacheTTL: config.cacheTTL ?? 3600,
requiredScopes: config.requiredScopes ?? [],
userIdClaim: config.userIdClaim ?? 'sub',
};
}
/**
* Get or create JWKS
*/
private getJWKS(): ReturnType<typeof createRemoteJWKSet> {
const now = Date.now();
const ttlMs = this.config.cacheTTL * 1000;
// Return cached JWKS if still valid
if (this.jwks && this.config.cacheKeys && (now - this.jwksCreatedAt) < ttlMs) {
return this.jwks;
}
// Create new JWKS
const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`);
this.jwks = createRemoteJWKSet(jwksUrl);
this.jwksCreatedAt = now;
return this.jwks;
}
/**
* Verify a JWT token
*/
async verify(token: string): Promise<VerifyResult> {
try {
const jwks = this.getJWKS();
const result: JWTVerifyResult = await jwtVerify(token, jwks, {
issuer: this.config.issuer,
audience: this.config.audience,
});
const payload = result.payload as unknown as JWTPayload;
// Check required scopes if configured
if (this.config.requiredScopes.length > 0) {
const tokenScopes = (payload.scope || '').split(' ').filter(Boolean);
const hasRequiredScope = this.config.requiredScopes.some(s => tokenScopes.includes(s));
if (!hasRequiredScope) {
return {
valid: false,
error: `Missing required scope. Required: ${this.config.requiredScopes.join(', ')}`,
};
}
}
return {
valid: true,
payload,
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Token verification failed';
return {
valid: false,
error: message,
};
}
}
/**
* Verify token and extract user info
*/
async authenticate(token: string): Promise<AuthenticatedUser | null> {
const result = await this.verify(token);
if (!result.valid || !result.payload) {
return null;
}
const payload = result.payload;
const scopes = (payload.scope || '').split(' ').filter(Boolean);
return {
id: payload[this.config.userIdClaim] as string || payload.sub,
userId: payload.userId,
tenantId: payload.tenantId,
email: payload.email,
username: payload.username,
roles: payload.roles || [],
scopes,
claims: payload,
};
}
/**
* Check if user has any of the specified scopes
*/
hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean {
const required = Array.isArray(scopes) ? scopes : [scopes];
return required.some(scope => user.scopes.includes(scope));
}
/**
* Check if user has all of the specified scopes
*/
hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean {
return scopes.every(scope => user.scopes.includes(scope));
}
/**
* Check if user has any of the specified roles
*/
hasRole(user: AuthenticatedUser, roles: string | string[]): boolean {
const required = Array.isArray(roles) ? roles : [roles];
return required.some(role => user.roles.includes(role));
}
/**
* Check if user has all of the specified roles
*/
hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean {
return roles.every(role => user.roles.includes(role));
}
/**
* Clear JWKS cache (force refresh on next verify)
*/
clearCache(): void {
this.jwks = null;
this.jwksCreatedAt = 0;
}
}
/**
* Create a new IAM verifier instance
*/
export function createIAMVerifier(config: IAMServerConfig): IAMVerifier {
return new IAMVerifier(config);
}

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

14
tsup.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: {
index: 'src/index.ts',
express: 'src/express/index.ts',
},
format: ['cjs', 'esm'],
dts: true,
clean: true,
splitting: false,
sourcemap: true,
external: ['express'],
})