230 lines
7.3 KiB
JavaScript
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 }
|