diff --git a/dist/express.d.mts b/dist/express.d.mts new file mode 100644 index 0000000..d4bbfb5 --- /dev/null +++ b/dist/express.d.mts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from 'express'; +import { AuthenticatedUser, IAMServerConfig, AuthOptions } from './index.mjs'; +export { IAMVerifier, createIAMVerifier } from './index.mjs'; + +/** + * Express middleware for IAM authentication + */ + +declare global { + namespace Express { + interface Request { + user?: AuthenticatedUser; + } + } +} +/** + * 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 + * }); + * ``` + */ +declare function createAuthMiddleware(config: IAMServerConfig): (options?: AuthOptions) => (req: Request, res: Response, next: NextFunction) => Promise> | undefined>; +/** + * 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); + * ``` + */ +declare function requireRole(roles: string | string[]): (req: Request, res: Response, next: NextFunction) => Response> | undefined; +/** + * Create scope-checking middleware (use after auth middleware) + * + * @example + * ```ts + * app.get('/data', auth(), requireScope('read:data'), handler); + * ``` + */ +declare function requireScope(scopes: string | string[]): (req: Request, res: Response, next: NextFunction) => Response> | undefined; +/** + * Optional auth - attaches user if token present, but doesn't require it + */ +declare function createOptionalAuthMiddleware(config: IAMServerConfig): (req: Request, res: Response, next: NextFunction) => Promise; + +export { AuthOptions, AuthenticatedUser, IAMServerConfig, createAuthMiddleware, createOptionalAuthMiddleware, requireRole, requireScope }; diff --git a/dist/express.d.ts b/dist/express.d.ts new file mode 100644 index 0000000..2114909 --- /dev/null +++ b/dist/express.d.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from 'express'; +import { AuthenticatedUser, IAMServerConfig, AuthOptions } from './index.js'; +export { IAMVerifier, createIAMVerifier } from './index.js'; + +/** + * Express middleware for IAM authentication + */ + +declare global { + namespace Express { + interface Request { + user?: AuthenticatedUser; + } + } +} +/** + * 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 + * }); + * ``` + */ +declare function createAuthMiddleware(config: IAMServerConfig): (options?: AuthOptions) => (req: Request, res: Response, next: NextFunction) => Promise> | undefined>; +/** + * 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); + * ``` + */ +declare function requireRole(roles: string | string[]): (req: Request, res: Response, next: NextFunction) => Response> | undefined; +/** + * Create scope-checking middleware (use after auth middleware) + * + * @example + * ```ts + * app.get('/data', auth(), requireScope('read:data'), handler); + * ``` + */ +declare function requireScope(scopes: string | string[]): (req: Request, res: Response, next: NextFunction) => Response> | undefined; +/** + * Optional auth - attaches user if token present, but doesn't require it + */ +declare function createOptionalAuthMiddleware(config: IAMServerConfig): (req: Request, res: Response, next: NextFunction) => Promise; + +export { AuthOptions, AuthenticatedUser, IAMServerConfig, createAuthMiddleware, createOptionalAuthMiddleware, requireRole, requireScope }; diff --git a/dist/express.js b/dist/express.js new file mode 100644 index 0000000..a0c4c8b --- /dev/null +++ b/dist/express.js @@ -0,0 +1,264 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/express/index.ts +var express_exports = {}; +__export(express_exports, { + IAMVerifier: () => IAMVerifier, + createAuthMiddleware: () => createAuthMiddleware, + createIAMVerifier: () => createIAMVerifier, + createOptionalAuthMiddleware: () => createOptionalAuthMiddleware, + requireRole: () => requireRole, + requireScope: () => requireScope +}); +module.exports = __toCommonJS(express_exports); + +// src/verifier.ts +var import_jose = require("jose"); +var IAMVerifier = class { + constructor(config) { + this.jwks = null; + this.jwksCreatedAt = 0; + 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 + */ + getJWKS() { + const now = Date.now(); + const ttlMs = this.config.cacheTTL * 1e3; + if (this.jwks && this.config.cacheKeys && now - this.jwksCreatedAt < ttlMs) { + return this.jwks; + } + const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`); + this.jwks = (0, import_jose.createRemoteJWKSet)(jwksUrl); + this.jwksCreatedAt = now; + return this.jwks; + } + /** + * Verify a JWT token + */ + async verify(token) { + try { + const jwks = this.getJWKS(); + const result = await (0, import_jose.jwtVerify)(token, jwks, { + issuer: this.config.issuer, + audience: this.config.audience + }); + const payload = result.payload; + 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) { + 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] || 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, scopes) { + 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, scopes) { + return scopes.every((scope) => user.scopes.includes(scope)); + } + /** + * Check if user has any of the specified roles + */ + hasRole(user, roles) { + 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, roles) { + return roles.every((role) => user.roles.includes(role)); + } + /** + * Clear JWKS cache (force refresh on next verify) + */ + clearCache() { + this.jwks = null; + this.jwksCreatedAt = 0; + } +}; +function createIAMVerifier(config) { + return new IAMVerifier(config); +} + +// src/express/index.ts +function extractToken(req) { + 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]; +} +function createAuthMiddleware(config) { + const verifier = createIAMVerifier(config); + return function auth(options = {}) { + return async (req, res, next) => { + 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" + }); + } + 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(", ")}` + }); + } + } + 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 ")}` + }); + } + } + req.user = user; + next(); + }; + }; +} +function requireRole(roles) { + const required = Array.isArray(roles) ? roles : [roles]; + return (req, res, next) => { + 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(); + }; +} +function requireScope(scopes) { + const required = Array.isArray(scopes) ? scopes : [scopes]; + return (req, res, next) => { + 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(); + }; +} +function createOptionalAuthMiddleware(config) { + const verifier = createIAMVerifier(config); + return async (req, res, next) => { + const token = extractToken(req); + if (token) { + const user = await verifier.authenticate(token); + if (user) { + req.user = user; + } + } + next(); + }; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + IAMVerifier, + createAuthMiddleware, + createIAMVerifier, + createOptionalAuthMiddleware, + requireRole, + requireScope +}); +//# sourceMappingURL=express.js.map \ No newline at end of file diff --git a/dist/express.js.map b/dist/express.js.map new file mode 100644 index 0000000..b51e0fd --- /dev/null +++ b/dist/express.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/express/index.ts","../src/verifier.ts"],"sourcesContent":["/**\n * Express middleware for IAM authentication\n */\n\nimport type { Request, Response, NextFunction } from 'express';\nimport { IAMVerifier, createIAMVerifier } from '../verifier';\nimport type { IAMServerConfig, AuthenticatedUser, AuthOptions } from '../types';\n\n// Extend Express Request to include user\ndeclare global {\n namespace Express {\n interface Request {\n user?: AuthenticatedUser;\n }\n }\n}\n\n/**\n * Extract Bearer token from Authorization header\n */\nfunction extractToken(req: Request): string | null {\n const authHeader = req.headers.authorization;\n if (!authHeader) return null;\n\n const parts = authHeader.split(' ');\n if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {\n return null;\n }\n\n return parts[1];\n}\n\n/**\n * Create authentication middleware\n * \n * @example\n * ```ts\n * import { createAuthMiddleware } from '@armco/iam-server/express';\n * \n * const auth = createAuthMiddleware({\n * issuer: 'http://localhost:5000',\n * audience: 'my-api',\n * });\n * \n * // Protect all routes\n * app.use('/api', auth());\n * \n * // Or specific routes\n * app.get('/api/users', auth(), (req, res) => {\n * console.log(req.user); // AuthenticatedUser\n * });\n * ```\n */\nexport function createAuthMiddleware(config: IAMServerConfig) {\n const verifier = createIAMVerifier(config);\n\n /**\n * Authentication middleware factory\n */\n return function auth(options: AuthOptions = {}) {\n return async (req: Request, res: Response, next: NextFunction) => {\n const token = extractToken(req);\n\n if (!token) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Missing or invalid Authorization header',\n });\n }\n\n const user = await verifier.authenticate(token);\n\n if (!user) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Invalid or expired token',\n });\n }\n\n // Check required scopes\n if (options.scopes && options.scopes.length > 0) {\n const hasScope = options.requireAllScopes\n ? verifier.hasAllScopes(user, options.scopes)\n : verifier.hasScope(user, options.scopes);\n\n if (!hasScope) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Insufficient scope. Required: ${options.scopes.join(', ')}`,\n });\n }\n }\n\n // Check required roles\n if (options.roles && options.roles.length > 0) {\n const hasRole = options.requireAllRoles\n ? verifier.hasAllRoles(user, options.roles)\n : verifier.hasRole(user, options.roles);\n\n if (!hasRole) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Insufficient permissions. Required role: ${options.roles.join(' or ')}`,\n });\n }\n }\n\n // Attach user to request\n req.user = user;\n next();\n };\n };\n}\n\n/**\n * Create role-checking middleware (use after auth middleware)\n * \n * @example\n * ```ts\n * app.get('/admin', auth(), requireRole('admin'), handler);\n * app.get('/editor', auth(), requireRole(['admin', 'editor']), handler);\n * ```\n */\nexport function requireRole(roles: string | string[]) {\n const required = Array.isArray(roles) ? roles : [roles];\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (!req.user) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Authentication required',\n });\n }\n\n const hasRole = required.some(role => req.user!.roles.includes(role));\n if (!hasRole) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Required role: ${required.join(' or ')}`,\n });\n }\n\n next();\n };\n}\n\n/**\n * Create scope-checking middleware (use after auth middleware)\n * \n * @example\n * ```ts\n * app.get('/data', auth(), requireScope('read:data'), handler);\n * ```\n */\nexport function requireScope(scopes: string | string[]) {\n const required = Array.isArray(scopes) ? scopes : [scopes];\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (!req.user) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Authentication required',\n });\n }\n\n const hasScope = required.some(scope => req.user!.scopes.includes(scope));\n if (!hasScope) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Required scope: ${required.join(' or ')}`,\n });\n }\n\n next();\n };\n}\n\n/**\n * Optional auth - attaches user if token present, but doesn't require it\n */\nexport function createOptionalAuthMiddleware(config: IAMServerConfig) {\n const verifier = createIAMVerifier(config);\n\n return async (req: Request, res: Response, next: NextFunction) => {\n const token = extractToken(req);\n\n if (token) {\n const user = await verifier.authenticate(token);\n if (user) {\n req.user = user;\n }\n }\n\n next();\n };\n}\n\n// Re-export types\nexport type { IAMServerConfig, AuthenticatedUser, AuthOptions } from '../types';\nexport { IAMVerifier, createIAMVerifier } from '../verifier';\n","/**\n * JWT Verifier using jose library\n * Fetches JWKS from IAM and validates tokens\n */\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyResult } from 'jose';\nimport type { IAMServerConfig, JWTPayload, VerifyResult, AuthenticatedUser } from './types';\n\nexport class IAMVerifier {\n private config: Required;\n private jwks: ReturnType | null = null;\n private jwksCreatedAt: number = 0;\n\n constructor(config: IAMServerConfig) {\n this.config = {\n issuer: config.issuer.replace(/\\/$/, ''),\n audience: config.audience,\n cacheKeys: config.cacheKeys ?? true,\n cacheTTL: config.cacheTTL ?? 3600,\n requiredScopes: config.requiredScopes ?? [],\n userIdClaim: config.userIdClaim ?? 'sub',\n };\n }\n\n /**\n * Get or create JWKS\n */\n private getJWKS(): ReturnType {\n const now = Date.now();\n const ttlMs = this.config.cacheTTL * 1000;\n\n // Return cached JWKS if still valid\n if (this.jwks && this.config.cacheKeys && (now - this.jwksCreatedAt) < ttlMs) {\n return this.jwks;\n }\n\n // Create new JWKS\n const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`);\n this.jwks = createRemoteJWKSet(jwksUrl);\n this.jwksCreatedAt = now;\n\n return this.jwks;\n }\n\n /**\n * Verify a JWT token\n */\n async verify(token: string): Promise {\n try {\n const jwks = this.getJWKS();\n\n const result: JWTVerifyResult = await jwtVerify(token, jwks, {\n issuer: this.config.issuer,\n audience: this.config.audience,\n });\n\n const payload = result.payload as unknown as JWTPayload;\n\n // Check required scopes if configured\n if (this.config.requiredScopes.length > 0) {\n const tokenScopes = (payload.scope || '').split(' ').filter(Boolean);\n const hasRequiredScope = this.config.requiredScopes.some(s => tokenScopes.includes(s));\n \n if (!hasRequiredScope) {\n return {\n valid: false,\n error: `Missing required scope. Required: ${this.config.requiredScopes.join(', ')}`,\n };\n }\n }\n\n return {\n valid: true,\n payload,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token verification failed';\n return {\n valid: false,\n error: message,\n };\n }\n }\n\n /**\n * Verify token and extract user info\n */\n async authenticate(token: string): Promise {\n const result = await this.verify(token);\n \n if (!result.valid || !result.payload) {\n return null;\n }\n\n const payload = result.payload;\n const scopes = (payload.scope || '').split(' ').filter(Boolean);\n\n return {\n id: payload[this.config.userIdClaim] as string || payload.sub,\n userId: payload.userId,\n tenantId: payload.tenantId,\n email: payload.email,\n username: payload.username,\n roles: payload.roles || [],\n scopes,\n claims: payload,\n };\n }\n\n /**\n * Check if user has any of the specified scopes\n */\n hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean {\n const required = Array.isArray(scopes) ? scopes : [scopes];\n return required.some(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has all of the specified scopes\n */\n hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean {\n return scopes.every(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has any of the specified roles\n */\n hasRole(user: AuthenticatedUser, roles: string | string[]): boolean {\n const required = Array.isArray(roles) ? roles : [roles];\n return required.some(role => user.roles.includes(role));\n }\n\n /**\n * Check if user has all of the specified roles\n */\n hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean {\n return roles.every(role => user.roles.includes(role));\n }\n\n /**\n * Clear JWKS cache (force refresh on next verify)\n */\n clearCache(): void {\n this.jwks = null;\n this.jwksCreatedAt = 0;\n }\n}\n\n/**\n * Create a new IAM verifier instance\n */\nexport function createIAMVerifier(config: IAMServerConfig): IAMVerifier {\n return new IAMVerifier(config);\n}\n"],"mappings":"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","names":[]} \ No newline at end of file diff --git a/dist/express.mjs b/dist/express.mjs new file mode 100644 index 0000000..d638161 --- /dev/null +++ b/dist/express.mjs @@ -0,0 +1,232 @@ +// src/verifier.ts +import { createRemoteJWKSet, jwtVerify } from "jose"; +var IAMVerifier = class { + constructor(config) { + this.jwks = null; + this.jwksCreatedAt = 0; + 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 + */ + getJWKS() { + const now = Date.now(); + const ttlMs = this.config.cacheTTL * 1e3; + if (this.jwks && this.config.cacheKeys && now - this.jwksCreatedAt < ttlMs) { + return this.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) { + try { + const jwks = this.getJWKS(); + const result = await jwtVerify(token, jwks, { + issuer: this.config.issuer, + audience: this.config.audience + }); + const payload = result.payload; + 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) { + 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] || 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, scopes) { + 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, scopes) { + return scopes.every((scope) => user.scopes.includes(scope)); + } + /** + * Check if user has any of the specified roles + */ + hasRole(user, roles) { + 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, roles) { + return roles.every((role) => user.roles.includes(role)); + } + /** + * Clear JWKS cache (force refresh on next verify) + */ + clearCache() { + this.jwks = null; + this.jwksCreatedAt = 0; + } +}; +function createIAMVerifier(config) { + return new IAMVerifier(config); +} + +// src/express/index.ts +function extractToken(req) { + 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]; +} +function createAuthMiddleware(config) { + const verifier = createIAMVerifier(config); + return function auth(options = {}) { + return async (req, res, next) => { + 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" + }); + } + 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(", ")}` + }); + } + } + 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 ")}` + }); + } + } + req.user = user; + next(); + }; + }; +} +function requireRole(roles) { + const required = Array.isArray(roles) ? roles : [roles]; + return (req, res, next) => { + 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(); + }; +} +function requireScope(scopes) { + const required = Array.isArray(scopes) ? scopes : [scopes]; + return (req, res, next) => { + 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(); + }; +} +function createOptionalAuthMiddleware(config) { + const verifier = createIAMVerifier(config); + return async (req, res, next) => { + const token = extractToken(req); + if (token) { + const user = await verifier.authenticate(token); + if (user) { + req.user = user; + } + } + next(); + }; +} +export { + IAMVerifier, + createAuthMiddleware, + createIAMVerifier, + createOptionalAuthMiddleware, + requireRole, + requireScope +}; +//# sourceMappingURL=express.mjs.map \ No newline at end of file diff --git a/dist/express.mjs.map b/dist/express.mjs.map new file mode 100644 index 0000000..4edb194 --- /dev/null +++ b/dist/express.mjs.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/verifier.ts","../src/express/index.ts"],"sourcesContent":["/**\n * JWT Verifier using jose library\n * Fetches JWKS from IAM and validates tokens\n */\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyResult } from 'jose';\nimport type { IAMServerConfig, JWTPayload, VerifyResult, AuthenticatedUser } from './types';\n\nexport class IAMVerifier {\n private config: Required;\n private jwks: ReturnType | null = null;\n private jwksCreatedAt: number = 0;\n\n constructor(config: IAMServerConfig) {\n this.config = {\n issuer: config.issuer.replace(/\\/$/, ''),\n audience: config.audience,\n cacheKeys: config.cacheKeys ?? true,\n cacheTTL: config.cacheTTL ?? 3600,\n requiredScopes: config.requiredScopes ?? [],\n userIdClaim: config.userIdClaim ?? 'sub',\n };\n }\n\n /**\n * Get or create JWKS\n */\n private getJWKS(): ReturnType {\n const now = Date.now();\n const ttlMs = this.config.cacheTTL * 1000;\n\n // Return cached JWKS if still valid\n if (this.jwks && this.config.cacheKeys && (now - this.jwksCreatedAt) < ttlMs) {\n return this.jwks;\n }\n\n // Create new JWKS\n const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`);\n this.jwks = createRemoteJWKSet(jwksUrl);\n this.jwksCreatedAt = now;\n\n return this.jwks;\n }\n\n /**\n * Verify a JWT token\n */\n async verify(token: string): Promise {\n try {\n const jwks = this.getJWKS();\n\n const result: JWTVerifyResult = await jwtVerify(token, jwks, {\n issuer: this.config.issuer,\n audience: this.config.audience,\n });\n\n const payload = result.payload as unknown as JWTPayload;\n\n // Check required scopes if configured\n if (this.config.requiredScopes.length > 0) {\n const tokenScopes = (payload.scope || '').split(' ').filter(Boolean);\n const hasRequiredScope = this.config.requiredScopes.some(s => tokenScopes.includes(s));\n \n if (!hasRequiredScope) {\n return {\n valid: false,\n error: `Missing required scope. Required: ${this.config.requiredScopes.join(', ')}`,\n };\n }\n }\n\n return {\n valid: true,\n payload,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token verification failed';\n return {\n valid: false,\n error: message,\n };\n }\n }\n\n /**\n * Verify token and extract user info\n */\n async authenticate(token: string): Promise {\n const result = await this.verify(token);\n \n if (!result.valid || !result.payload) {\n return null;\n }\n\n const payload = result.payload;\n const scopes = (payload.scope || '').split(' ').filter(Boolean);\n\n return {\n id: payload[this.config.userIdClaim] as string || payload.sub,\n userId: payload.userId,\n tenantId: payload.tenantId,\n email: payload.email,\n username: payload.username,\n roles: payload.roles || [],\n scopes,\n claims: payload,\n };\n }\n\n /**\n * Check if user has any of the specified scopes\n */\n hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean {\n const required = Array.isArray(scopes) ? scopes : [scopes];\n return required.some(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has all of the specified scopes\n */\n hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean {\n return scopes.every(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has any of the specified roles\n */\n hasRole(user: AuthenticatedUser, roles: string | string[]): boolean {\n const required = Array.isArray(roles) ? roles : [roles];\n return required.some(role => user.roles.includes(role));\n }\n\n /**\n * Check if user has all of the specified roles\n */\n hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean {\n return roles.every(role => user.roles.includes(role));\n }\n\n /**\n * Clear JWKS cache (force refresh on next verify)\n */\n clearCache(): void {\n this.jwks = null;\n this.jwksCreatedAt = 0;\n }\n}\n\n/**\n * Create a new IAM verifier instance\n */\nexport function createIAMVerifier(config: IAMServerConfig): IAMVerifier {\n return new IAMVerifier(config);\n}\n","/**\n * Express middleware for IAM authentication\n */\n\nimport type { Request, Response, NextFunction } from 'express';\nimport { IAMVerifier, createIAMVerifier } from '../verifier';\nimport type { IAMServerConfig, AuthenticatedUser, AuthOptions } from '../types';\n\n// Extend Express Request to include user\ndeclare global {\n namespace Express {\n interface Request {\n user?: AuthenticatedUser;\n }\n }\n}\n\n/**\n * Extract Bearer token from Authorization header\n */\nfunction extractToken(req: Request): string | null {\n const authHeader = req.headers.authorization;\n if (!authHeader) return null;\n\n const parts = authHeader.split(' ');\n if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {\n return null;\n }\n\n return parts[1];\n}\n\n/**\n * Create authentication middleware\n * \n * @example\n * ```ts\n * import { createAuthMiddleware } from '@armco/iam-server/express';\n * \n * const auth = createAuthMiddleware({\n * issuer: 'http://localhost:5000',\n * audience: 'my-api',\n * });\n * \n * // Protect all routes\n * app.use('/api', auth());\n * \n * // Or specific routes\n * app.get('/api/users', auth(), (req, res) => {\n * console.log(req.user); // AuthenticatedUser\n * });\n * ```\n */\nexport function createAuthMiddleware(config: IAMServerConfig) {\n const verifier = createIAMVerifier(config);\n\n /**\n * Authentication middleware factory\n */\n return function auth(options: AuthOptions = {}) {\n return async (req: Request, res: Response, next: NextFunction) => {\n const token = extractToken(req);\n\n if (!token) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Missing or invalid Authorization header',\n });\n }\n\n const user = await verifier.authenticate(token);\n\n if (!user) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Invalid or expired token',\n });\n }\n\n // Check required scopes\n if (options.scopes && options.scopes.length > 0) {\n const hasScope = options.requireAllScopes\n ? verifier.hasAllScopes(user, options.scopes)\n : verifier.hasScope(user, options.scopes);\n\n if (!hasScope) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Insufficient scope. Required: ${options.scopes.join(', ')}`,\n });\n }\n }\n\n // Check required roles\n if (options.roles && options.roles.length > 0) {\n const hasRole = options.requireAllRoles\n ? verifier.hasAllRoles(user, options.roles)\n : verifier.hasRole(user, options.roles);\n\n if (!hasRole) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Insufficient permissions. Required role: ${options.roles.join(' or ')}`,\n });\n }\n }\n\n // Attach user to request\n req.user = user;\n next();\n };\n };\n}\n\n/**\n * Create role-checking middleware (use after auth middleware)\n * \n * @example\n * ```ts\n * app.get('/admin', auth(), requireRole('admin'), handler);\n * app.get('/editor', auth(), requireRole(['admin', 'editor']), handler);\n * ```\n */\nexport function requireRole(roles: string | string[]) {\n const required = Array.isArray(roles) ? roles : [roles];\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (!req.user) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Authentication required',\n });\n }\n\n const hasRole = required.some(role => req.user!.roles.includes(role));\n if (!hasRole) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Required role: ${required.join(' or ')}`,\n });\n }\n\n next();\n };\n}\n\n/**\n * Create scope-checking middleware (use after auth middleware)\n * \n * @example\n * ```ts\n * app.get('/data', auth(), requireScope('read:data'), handler);\n * ```\n */\nexport function requireScope(scopes: string | string[]) {\n const required = Array.isArray(scopes) ? scopes : [scopes];\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (!req.user) {\n return res.status(401).json({\n error: 'unauthorized',\n message: 'Authentication required',\n });\n }\n\n const hasScope = required.some(scope => req.user!.scopes.includes(scope));\n if (!hasScope) {\n return res.status(403).json({\n error: 'forbidden',\n message: `Required scope: ${required.join(' or ')}`,\n });\n }\n\n next();\n };\n}\n\n/**\n * Optional auth - attaches user if token present, but doesn't require it\n */\nexport function createOptionalAuthMiddleware(config: IAMServerConfig) {\n const verifier = createIAMVerifier(config);\n\n return async (req: Request, res: Response, next: NextFunction) => {\n const token = extractToken(req);\n\n if (token) {\n const user = await verifier.authenticate(token);\n if (user) {\n req.user = user;\n }\n }\n\n next();\n };\n}\n\n// Re-export types\nexport type { IAMServerConfig, AuthenticatedUser, AuthOptions } from '../types';\nexport { IAMVerifier, createIAMVerifier } from '../verifier';\n"],"mappings":"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","names":[]} \ No newline at end of file diff --git a/dist/index.d.mts b/dist/index.d.mts new file mode 100644 index 0000000..a1e5853 --- /dev/null +++ b/dist/index.d.mts @@ -0,0 +1,128 @@ +/** + * @armco/iam-server Types + */ +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; +} +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; +} +interface VerifyResult { + valid: boolean; + payload?: JWTPayload; + error?: string; +} +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; +} +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; +} + +/** + * JWT Verifier using jose library + * Fetches JWKS from IAM and validates tokens + */ + +declare class IAMVerifier { + private config; + private jwks; + private jwksCreatedAt; + constructor(config: IAMServerConfig); + /** + * Get or create JWKS + */ + private getJWKS; + /** + * Verify a JWT token + */ + verify(token: string): Promise; + /** + * Verify token and extract user info + */ + authenticate(token: string): Promise; + /** + * Check if user has any of the specified scopes + */ + hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean; + /** + * Check if user has all of the specified scopes + */ + hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean; + /** + * Check if user has any of the specified roles + */ + hasRole(user: AuthenticatedUser, roles: string | string[]): boolean; + /** + * Check if user has all of the specified roles + */ + hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean; + /** + * Clear JWKS cache (force refresh on next verify) + */ + clearCache(): void; +} +/** + * Create a new IAM verifier instance + */ +declare function createIAMVerifier(config: IAMServerConfig): IAMVerifier; + +export { type AuthOptions, type AuthenticatedUser, type IAMServerConfig, IAMVerifier, type JWTPayload, type VerifyResult, createIAMVerifier }; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..a1e5853 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,128 @@ +/** + * @armco/iam-server Types + */ +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; +} +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; +} +interface VerifyResult { + valid: boolean; + payload?: JWTPayload; + error?: string; +} +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; +} +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; +} + +/** + * JWT Verifier using jose library + * Fetches JWKS from IAM and validates tokens + */ + +declare class IAMVerifier { + private config; + private jwks; + private jwksCreatedAt; + constructor(config: IAMServerConfig); + /** + * Get or create JWKS + */ + private getJWKS; + /** + * Verify a JWT token + */ + verify(token: string): Promise; + /** + * Verify token and extract user info + */ + authenticate(token: string): Promise; + /** + * Check if user has any of the specified scopes + */ + hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean; + /** + * Check if user has all of the specified scopes + */ + hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean; + /** + * Check if user has any of the specified roles + */ + hasRole(user: AuthenticatedUser, roles: string | string[]): boolean; + /** + * Check if user has all of the specified roles + */ + hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean; + /** + * Clear JWKS cache (force refresh on next verify) + */ + clearCache(): void; +} +/** + * Create a new IAM verifier instance + */ +declare function createIAMVerifier(config: IAMServerConfig): IAMVerifier; + +export { type AuthOptions, type AuthenticatedUser, type IAMServerConfig, IAMVerifier, type JWTPayload, type VerifyResult, createIAMVerifier }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..dfaa5cc --- /dev/null +++ b/dist/index.js @@ -0,0 +1,153 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/index.ts +var src_exports = {}; +__export(src_exports, { + IAMVerifier: () => IAMVerifier, + createIAMVerifier: () => createIAMVerifier +}); +module.exports = __toCommonJS(src_exports); + +// src/verifier.ts +var import_jose = require("jose"); +var IAMVerifier = class { + constructor(config) { + this.jwks = null; + this.jwksCreatedAt = 0; + 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 + */ + getJWKS() { + const now = Date.now(); + const ttlMs = this.config.cacheTTL * 1e3; + if (this.jwks && this.config.cacheKeys && now - this.jwksCreatedAt < ttlMs) { + return this.jwks; + } + const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`); + this.jwks = (0, import_jose.createRemoteJWKSet)(jwksUrl); + this.jwksCreatedAt = now; + return this.jwks; + } + /** + * Verify a JWT token + */ + async verify(token) { + try { + const jwks = this.getJWKS(); + const result = await (0, import_jose.jwtVerify)(token, jwks, { + issuer: this.config.issuer, + audience: this.config.audience + }); + const payload = result.payload; + 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) { + 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] || 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, scopes) { + 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, scopes) { + return scopes.every((scope) => user.scopes.includes(scope)); + } + /** + * Check if user has any of the specified roles + */ + hasRole(user, roles) { + 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, roles) { + return roles.every((role) => user.roles.includes(role)); + } + /** + * Clear JWKS cache (force refresh on next verify) + */ + clearCache() { + this.jwks = null; + this.jwksCreatedAt = 0; + } +}; +function createIAMVerifier(config) { + return new IAMVerifier(config); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + IAMVerifier, + createIAMVerifier +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..69d180c --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/index.ts","../src/verifier.ts"],"sourcesContent":["/**\n * @armco/iam-server\n * \n * Server-side JWT validation and utilities for IAM.\n * \n * @example\n * ```ts\n * import { createIAMVerifier } from '@armco/iam-server';\n * \n * const verifier = createIAMVerifier({\n * issuer: 'http://localhost:5000',\n * audience: 'my-api',\n * });\n * \n * // Verify a token\n * const result = await verifier.verify(token);\n * if (result.valid) {\n * console.log(result.payload);\n * }\n * \n * // Or authenticate and get user info\n * const user = await verifier.authenticate(token);\n * if (user) {\n * console.log(user.email, user.roles);\n * }\n * ```\n * \n * For Express middleware, use:\n * ```ts\n * import { createAuthMiddleware } from '@armco/iam-server/express';\n * ```\n */\n\nexport { IAMVerifier, createIAMVerifier } from './verifier';\nexport type {\n IAMServerConfig,\n JWTPayload,\n VerifyResult,\n AuthenticatedUser,\n AuthOptions,\n} from './types';\n","/**\n * JWT Verifier using jose library\n * Fetches JWKS from IAM and validates tokens\n */\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyResult } from 'jose';\nimport type { IAMServerConfig, JWTPayload, VerifyResult, AuthenticatedUser } from './types';\n\nexport class IAMVerifier {\n private config: Required;\n private jwks: ReturnType | null = null;\n private jwksCreatedAt: number = 0;\n\n constructor(config: IAMServerConfig) {\n this.config = {\n issuer: config.issuer.replace(/\\/$/, ''),\n audience: config.audience,\n cacheKeys: config.cacheKeys ?? true,\n cacheTTL: config.cacheTTL ?? 3600,\n requiredScopes: config.requiredScopes ?? [],\n userIdClaim: config.userIdClaim ?? 'sub',\n };\n }\n\n /**\n * Get or create JWKS\n */\n private getJWKS(): ReturnType {\n const now = Date.now();\n const ttlMs = this.config.cacheTTL * 1000;\n\n // Return cached JWKS if still valid\n if (this.jwks && this.config.cacheKeys && (now - this.jwksCreatedAt) < ttlMs) {\n return this.jwks;\n }\n\n // Create new JWKS\n const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`);\n this.jwks = createRemoteJWKSet(jwksUrl);\n this.jwksCreatedAt = now;\n\n return this.jwks;\n }\n\n /**\n * Verify a JWT token\n */\n async verify(token: string): Promise {\n try {\n const jwks = this.getJWKS();\n\n const result: JWTVerifyResult = await jwtVerify(token, jwks, {\n issuer: this.config.issuer,\n audience: this.config.audience,\n });\n\n const payload = result.payload as unknown as JWTPayload;\n\n // Check required scopes if configured\n if (this.config.requiredScopes.length > 0) {\n const tokenScopes = (payload.scope || '').split(' ').filter(Boolean);\n const hasRequiredScope = this.config.requiredScopes.some(s => tokenScopes.includes(s));\n \n if (!hasRequiredScope) {\n return {\n valid: false,\n error: `Missing required scope. Required: ${this.config.requiredScopes.join(', ')}`,\n };\n }\n }\n\n return {\n valid: true,\n payload,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token verification failed';\n return {\n valid: false,\n error: message,\n };\n }\n }\n\n /**\n * Verify token and extract user info\n */\n async authenticate(token: string): Promise {\n const result = await this.verify(token);\n \n if (!result.valid || !result.payload) {\n return null;\n }\n\n const payload = result.payload;\n const scopes = (payload.scope || '').split(' ').filter(Boolean);\n\n return {\n id: payload[this.config.userIdClaim] as string || payload.sub,\n userId: payload.userId,\n tenantId: payload.tenantId,\n email: payload.email,\n username: payload.username,\n roles: payload.roles || [],\n scopes,\n claims: payload,\n };\n }\n\n /**\n * Check if user has any of the specified scopes\n */\n hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean {\n const required = Array.isArray(scopes) ? scopes : [scopes];\n return required.some(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has all of the specified scopes\n */\n hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean {\n return scopes.every(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has any of the specified roles\n */\n hasRole(user: AuthenticatedUser, roles: string | string[]): boolean {\n const required = Array.isArray(roles) ? roles : [roles];\n return required.some(role => user.roles.includes(role));\n }\n\n /**\n * Check if user has all of the specified roles\n */\n hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean {\n return roles.every(role => user.roles.includes(role));\n }\n\n /**\n * Clear JWKS cache (force refresh on next verify)\n */\n clearCache(): void {\n this.jwks = null;\n this.jwksCreatedAt = 0;\n }\n}\n\n/**\n * Create a new IAM verifier instance\n */\nexport function createIAMVerifier(config: IAMServerConfig): IAMVerifier {\n return new IAMVerifier(config);\n}\n"],"mappings":"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","names":[]} \ No newline at end of file diff --git a/dist/index.mjs b/dist/index.mjs new file mode 100644 index 0000000..561f293 --- /dev/null +++ b/dist/index.mjs @@ -0,0 +1,125 @@ +// src/verifier.ts +import { createRemoteJWKSet, jwtVerify } from "jose"; +var IAMVerifier = class { + constructor(config) { + this.jwks = null; + this.jwksCreatedAt = 0; + 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 + */ + getJWKS() { + const now = Date.now(); + const ttlMs = this.config.cacheTTL * 1e3; + if (this.jwks && this.config.cacheKeys && now - this.jwksCreatedAt < ttlMs) { + return this.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) { + try { + const jwks = this.getJWKS(); + const result = await jwtVerify(token, jwks, { + issuer: this.config.issuer, + audience: this.config.audience + }); + const payload = result.payload; + 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) { + 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] || 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, scopes) { + 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, scopes) { + return scopes.every((scope) => user.scopes.includes(scope)); + } + /** + * Check if user has any of the specified roles + */ + hasRole(user, roles) { + 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, roles) { + return roles.every((role) => user.roles.includes(role)); + } + /** + * Clear JWKS cache (force refresh on next verify) + */ + clearCache() { + this.jwks = null; + this.jwksCreatedAt = 0; + } +}; +function createIAMVerifier(config) { + return new IAMVerifier(config); +} +export { + IAMVerifier, + createIAMVerifier +}; +//# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/dist/index.mjs.map b/dist/index.mjs.map new file mode 100644 index 0000000..1cca5c8 --- /dev/null +++ b/dist/index.mjs.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/verifier.ts"],"sourcesContent":["/**\n * JWT Verifier using jose library\n * Fetches JWKS from IAM and validates tokens\n */\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyResult } from 'jose';\nimport type { IAMServerConfig, JWTPayload, VerifyResult, AuthenticatedUser } from './types';\n\nexport class IAMVerifier {\n private config: Required;\n private jwks: ReturnType | null = null;\n private jwksCreatedAt: number = 0;\n\n constructor(config: IAMServerConfig) {\n this.config = {\n issuer: config.issuer.replace(/\\/$/, ''),\n audience: config.audience,\n cacheKeys: config.cacheKeys ?? true,\n cacheTTL: config.cacheTTL ?? 3600,\n requiredScopes: config.requiredScopes ?? [],\n userIdClaim: config.userIdClaim ?? 'sub',\n };\n }\n\n /**\n * Get or create JWKS\n */\n private getJWKS(): ReturnType {\n const now = Date.now();\n const ttlMs = this.config.cacheTTL * 1000;\n\n // Return cached JWKS if still valid\n if (this.jwks && this.config.cacheKeys && (now - this.jwksCreatedAt) < ttlMs) {\n return this.jwks;\n }\n\n // Create new JWKS\n const jwksUrl = new URL(`${this.config.issuer}/.well-known/jwks.json`);\n this.jwks = createRemoteJWKSet(jwksUrl);\n this.jwksCreatedAt = now;\n\n return this.jwks;\n }\n\n /**\n * Verify a JWT token\n */\n async verify(token: string): Promise {\n try {\n const jwks = this.getJWKS();\n\n const result: JWTVerifyResult = await jwtVerify(token, jwks, {\n issuer: this.config.issuer,\n audience: this.config.audience,\n });\n\n const payload = result.payload as unknown as JWTPayload;\n\n // Check required scopes if configured\n if (this.config.requiredScopes.length > 0) {\n const tokenScopes = (payload.scope || '').split(' ').filter(Boolean);\n const hasRequiredScope = this.config.requiredScopes.some(s => tokenScopes.includes(s));\n \n if (!hasRequiredScope) {\n return {\n valid: false,\n error: `Missing required scope. Required: ${this.config.requiredScopes.join(', ')}`,\n };\n }\n }\n\n return {\n valid: true,\n payload,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Token verification failed';\n return {\n valid: false,\n error: message,\n };\n }\n }\n\n /**\n * Verify token and extract user info\n */\n async authenticate(token: string): Promise {\n const result = await this.verify(token);\n \n if (!result.valid || !result.payload) {\n return null;\n }\n\n const payload = result.payload;\n const scopes = (payload.scope || '').split(' ').filter(Boolean);\n\n return {\n id: payload[this.config.userIdClaim] as string || payload.sub,\n userId: payload.userId,\n tenantId: payload.tenantId,\n email: payload.email,\n username: payload.username,\n roles: payload.roles || [],\n scopes,\n claims: payload,\n };\n }\n\n /**\n * Check if user has any of the specified scopes\n */\n hasScope(user: AuthenticatedUser, scopes: string | string[]): boolean {\n const required = Array.isArray(scopes) ? scopes : [scopes];\n return required.some(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has all of the specified scopes\n */\n hasAllScopes(user: AuthenticatedUser, scopes: string[]): boolean {\n return scopes.every(scope => user.scopes.includes(scope));\n }\n\n /**\n * Check if user has any of the specified roles\n */\n hasRole(user: AuthenticatedUser, roles: string | string[]): boolean {\n const required = Array.isArray(roles) ? roles : [roles];\n return required.some(role => user.roles.includes(role));\n }\n\n /**\n * Check if user has all of the specified roles\n */\n hasAllRoles(user: AuthenticatedUser, roles: string[]): boolean {\n return roles.every(role => user.roles.includes(role));\n }\n\n /**\n * Clear JWKS cache (force refresh on next verify)\n */\n clearCache(): void {\n this.jwks = null;\n this.jwksCreatedAt = 0;\n }\n}\n\n/**\n * Create a new IAM verifier instance\n */\nexport function createIAMVerifier(config: IAMServerConfig): IAMVerifier {\n return new IAMVerifier(config);\n}\n"],"mappings":"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","names":[]} \ No newline at end of file diff --git a/package.json b/package.json index e7ae3af..914f2a4 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,14 @@ "types": "dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.mjs", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "require": "./dist/index.js" }, "./express": { + "types": "./dist/express.d.ts", "import": "./dist/express.mjs", - "require": "./dist/express.js", - "types": "./dist/express.d.ts" + "require": "./dist/express.js" } }, "files": [