Files
wiki-plugin-useraccesstokens/index.js
Christian Galo d88a0e3bc6
Some checks failed
CI / build (20.x) (push) Has been cancelled
CI / build (22.x) (push) Has been cancelled
isAdmin implemented.
2025-07-27 19:11:17 -05:00

230 lines
7.3 KiB
JavaScript

/**
* 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 }