/** * Wiki Plugin: User Access Tokens * * This plugin provides User Access Token functionality as a security enhancer * for the wiki-security-composable system. It is designed to work only with * the composable security architecture, not as a standalone plugin. */ import { TokenManager } from './server/server.js' /** * Security enhancer functionality * Called by wiki-security-composable when this plugin is used as an auth enhancer */ export const securityEnhancer = (log, loga, argv, baseHandler) => { const enhancer = {} // Initialize token manager const tokenManager = new TokenManager(argv.status) // Get admin configuration from argv (same way other security plugins do) const admin = argv.admin // Helper function to get current user from enhanced authentication const getCurrentUser = (req) => { return enhancer.getUser(req, () => baseHandler.getUser(req)) } // Enhanced authorization check that includes token-based auth enhancer.isAuthorized = (req, baseIsAuthorized) => { // First check if base authentication already authorizes if (baseIsAuthorized()) { return true } // Check for token-based authorization if (req.tokenAuth && req.tokenAuth.user) { return true } return false } // Enhanced admin check that includes token-based admin enhancer.isAdmin = (req, baseIsAdmin) => { // First check if base authentication already grants admin if (baseIsAdmin()) { return true } // Check if token belongs to admin user if (req.tokenAuth && req.tokenAuth.user && admin !== undefined) { // Compare token user with configured admin // Use JSON.stringify for deep comparison like we do elsewhere return JSON.stringify(req.tokenAuth.user) === JSON.stringify(admin) } return false } // Enhanced user identification that includes token users enhancer.getUser = (req, baseGetUser) => { // If we have token auth, return the token user if (req.tokenAuth && req.tokenAuth.user) { return req.tokenAuth.user } // Otherwise, use base authentication return baseGetUser() } // Helper function to extract Bearer token from Authorization header // Implements robust parsing according to HTTP specification: // - Case-insensitive header name and scheme // - Tolerates whitespace after colon and between scheme and token const extractBearerToken = (req) => { // Get authorization header (case-insensitive) const authHeader = Object.keys(req.headers) .find(key => key.toLowerCase() === 'authorization') if (!authHeader || !req.headers[authHeader]) { return null } const headerValue = req.headers[authHeader] // Parse Authorization header with regex for robustness // Pattern: optional whitespace, "bearer" (case-insensitive), required whitespace, token const bearerMatch = headerValue.match(/^\s*bearer\s+(.+)$/i) if (bearerMatch) { return bearerMatch[1].trim() // Return the token part, trimmed } return null } // Middleware for token validation - this runs on all requests enhancer.middleware = async (req, res, next) => { const token = extractBearerToken(req) if (token) { try { const tokenRecord = await tokenManager.validateToken(token) if (tokenRecord) { // Set token auth context for use by enhanced methods req.tokenAuth = { user: tokenRecord.user, scopes: tokenRecord.scopes, tokenName: tokenRecord.name } } } catch (error) { console.error('Token validation error:', error) } } next() } // Additional routes for token management enhancer.defineRoutes = (app, cors, updateOwner) => { // Middleware to check if user is authenticated via enhanced handler const authenticated = function (req, res, next) { // Use the enhanced authorization that includes token auth const isAuthorized = enhancer.isAuthorized(req, () => { return baseHandler.isAuthorized(req) }) if (!isAuthorized) { return res.status(401).json({ error: 'Must be authenticated' }) } return next() } // POST /plugin/useraccesstokens/create - Create a new token app.post('/plugin/useraccesstokens/create', authenticated, async (req, res) => { try { const user = getCurrentUser(req) const { name, expiresInDays, scopes } = req.body if (!name || typeof name !== 'string' || name.trim() === '') { return res.status(400).json({ error: 'Token name is required' }) } // Validate scopes if provided if (scopes && !Array.isArray(scopes)) { return res.status(400).json({ error: 'Scopes must be an array' }) } // Filter valid scopes - admin scope is not needed since admin is based on user const validScopes = ['site:read', 'site:write'] const requestedScopes = scopes || ['site:read', 'site:write'] const filteredScopes = requestedScopes.filter(scope => validScopes.includes(scope)) if (requestedScopes.length > 0 && filteredScopes.length === 0) { return res.status(400).json({ error: 'No valid scopes provided' }) } const result = await tokenManager.createToken(user, name.trim(), expiresInDays, filteredScopes) res.json({ token: result.token, name: result.record.name, displayHint: result.record.displayHint, created: result.record.created, expires: result.record.expires, scopes: result.record.scopes }) } catch (error) { res.status(400).json({ error: error.message }) } }) // GET /plugin/useraccesstokens/list - List user's tokens app.get('/plugin/useraccesstokens/list', authenticated, async (req, res) => { try { const user = getCurrentUser(req) const tokens = await tokenManager.listTokens(user) res.json(tokens) } catch (error) { res.status(500).json({ error: error.message }) } }) // POST /plugin/useraccesstokens/revoke - Revoke a token app.post('/plugin/useraccesstokens/revoke', authenticated, async (req, res) => { try { const user = getCurrentUser(req) const { name } = req.body if (!name) { return res.status(400).json({ error: 'Token name is required' }) } await tokenManager.revokeToken(user, name) res.json({ message: `Token "${name}" revoked successfully` }) } catch (error) { res.status(400).json({ error: error.message }) } }) // DELETE /plugin/useraccesstokens/delete - Delete a token app.delete('/plugin/useraccesstokens/delete', authenticated, async (req, res) => { try { const user = getCurrentUser(req) const { name } = req.body if (!name) { return res.status(400).json({ error: 'Token name is required' }) } await tokenManager.deleteToken(user, name) res.json({ message: `Token "${name}" deleted successfully` }) } catch (error) { res.status(400).json({ error: error.message }) } }) log('UserAccessTokens security enhancer routes registered') } log('UserAccessTokens security enhancer initialized') return enhancer } // Export TokenManager for testing and direct use export { TokenManager }