# Wiki Security Composable ## Description **`wiki-security-composable` is a composable security plugin that separates authentication from authorization** while maintaining full compatibility with the existing wiki server. This architecture allows you to mix and match authentication providers (like friends or passportjs) with multiple authorization enhancers (like user access tokens, rate limiting, etc.) to create a flexible security system. ### Components #### Security Plugin **`wiki-security-composable`** - The foundation plugin that: - Loads a base authentication provider (friends, passportjs, etc.) - Loads multiple authorization enhancers - Composes them into a single security interface - Maintains compatibility with existing wiki server expectations The plugin uses a **function composition pattern** where authorization enhancers wrap the base authentication provider's methods. Each enhancer receives both the original request and a reference to the previous function in the chain, allowing it to call, modify, or completely override the previous behavior. This creates a middleware-like system where enhancers can participate in user identification, authorization checks, and admin verification. **Order matters**: Enhancers are processed sequentially, with each one wrapping the previous function. The final composed function represents a chain where the last enhancer is called first. This means enhancers later in the configuration list can override decisions of earlier enhancers. For example, a rate limiting enhancer should typically come last to ensure it can block requests regardless of other authorization decisions. #### Authorization Enhancers Authorization enhancers are implemented as regular `wiki-plugin-*` packages that export a `securityEnhancer` function. This approach integrates seamlessly with the existing plugin system and allows plugins to provide both UI components and server-side functionality. **`wiki-plugin-useraccesstokens`** - Plugin that adds API token authentication and provides token management (create, list, revoke, delete) for any base authentication provider. **`wiki-plugin-ratelimit`** - Plugin that adds request rate limiting to prevent abuse and ensure fair usage. ### Context Security plugins in FedWiki have traditionally been monolithic, making it difficult to extend or customize security features. This composable architecture addresses that by allowing developers to create modular security components that can be combined as needed. The need for this arose from the desire to add new security features without modifying the core wiki server or existing plugins. By separating authentication from authorization, we can create a flexible system that allows for easy extension and customization. This composable architecture could later be adopted by the core FedWiki server, but now it serves as a proof of concept and practical implementation for developers to build upon. #### Other approaches considered In our quest to add User Access Tokens and Rate Limiting to FedWiki, we explored several approaches: 1. **Stacking Wrapper Security Plugins**: This approach involved creating new plugins that wrapped existing authentication plugins. As an example, we could create a `wiki-security-useraccesstokens` plugin that wraps the `wiki-security-passportjs` plugin to add token support. While this would allow for adding multiple authorization features to an existing plugin, it would work by stacking wrapper plugins, which could lead to complexity and compatibility issues. We didn't want a situation where you would have to wrap `wiki-security-friends` with `wiki-security-useraccesstokens` and then wrap that with `wiki-security-ratelimit`, etc. 2. **Multiple Security Plugins**: This approach involved creating authorization enhancers as security plugins that could be installed alongside existing authentication plugins. For example, we could have a `wiki-security-useraccesstokens` plugin that works with any base authentication provider security plugin. This would've been implemented by making changes to the core FedWiki server to allow multiple security plugins to be loaded and composed together, making no distinction between authentication and authorization. This would have required significant changes to the core server and made it very complex to manage multiple security plugins and their interactions. We ultimately decided against these approaches because they either added unnecessary complexity. Instead, we opted for a composable security architecture that separates authentication from authorization while maintaining full compatibility with the wiki server and existing plugins. ## Usage ### Basic Setup (Friends + Tokens) Install the necessary components: ```bash npm install wiki-security-composable wiki-plugin-useraccesstokens ``` Configure wiki `config.json`: ```JSON { "security_type": "wiki-security-composable", "auth_provider": "wiki-security-friends", "authz_enhancers": ["wiki-plugin-useraccesstokens"] } ``` Run the wiki server: ```bash wiki --security_type composable ``` Or configure directly from command line: ```bash wiki --security_type composable --auth_provider wiki-security-friends --authz_enhancers wiki-plugin-useraccesstokens ``` ### Co-op Setup (OAuth + Tokens + Rate Limiting) Install the necessary components: ```bash npm install wiki-security-composable wiki-plugin-useraccesstokens wiki-plugin-ratelimit ``` Configure wiki `config.json`: ```JSON { "security_type": "wiki-security-composable", "auth_provider": "wiki-security-passportjs", "authz_enhancers": [ "wiki-plugin-useraccesstokens", "wiki-plugin-ratelimit" ], "ratelimit_config": { "windowMs": 900000, // 15 minutes "maxRequests": 1000, "maxAuthRequests": 5 } } ``` ### Migration from Existing Setups #### From Friends Plugin Current setup: ```JSON { "security_type": "wiki-security-friends" } ``` Migrated setup (no functional change): ```JSON { "security_type": "wiki-security-composable", "auth_provider": "wiki-security-friends" } ``` Enhanced setup (adds API tokens): ```JSON { "security_type": "wiki-security-composable", "auth_provider": "wiki-security-friends", "authz_enhancers": ["wiki-plugin-useraccesstokens"] } ``` #### From PassportJS Plugin Current setup: ```JSON { "security_type": "wiki-security-passportjs", "google_clientid": "...", "google_clientsecret": "..." } ``` Migrated setup: ```JSON { "security_type": "wiki-security-composable", "auth_provider": "wiki-security-passportjs", "google_clientid": "...", "google_clientsecret": "..." } ``` ## Development ### Creating New Authentication Providers To create a new authentication provider, implement the standard security plugin interface: ```javascript module.exports = (log, loga, argv) => ({ retrieveOwner(cb), getOwner(), setOwner(id, cb), getUser(req), isAuthorized(req), isAdmin(req), defineRoutes(app, cors, updateOwner) }) ``` ### Creating New Authorization Enhancers Authorization enhancers are implemented as regular plugins by exporting a `securityEnhancer` function. Create a regular `wiki-plugin-*` package and export a `securityEnhancer` function: ```javascript // index.js - Main plugin entry point export const startServer = (params) => { // Regular plugin server functionality (optional) console.log('Plugin server started') } export const securityEnhancer = (log, loga, argv, baseHandler) => { const enhancer = {} enhancer.getUser = (req, baseGetUser) => { // Call base handler to get user return baseGetUser() } // Override authorization to check additional permissions enhancer.isAuthorized = (req, baseIsAuthorized) => { const baseResult = baseIsAuthorized() if (!baseResult) return false // Additional authorization logic here return checkCustomPermissions(req) } // Override admin check enhancer.isAdmin = (req, baseIsAdmin) => { const baseResult = baseIsAdmin() if (!baseResult) return false // Additional admin checks return checkAdminPermissions(req) } // Add middleware (optional) enhancer.middleware = (req, res, next) => { // Custom middleware logic next() } // Add additional routes (optional) enhancer.defineRoutes = (app, cors, updateOwner) => { app.get('/auth/custom', (req, res) => { res.json({ message: 'Custom endpoint' }) }) } return enhancer } ``` Then use it in configuration: ```json { "security_type": "wiki-security-composable", "auth_provider": "wiki-security-friends", "authz_enhancers": ["wiki-plugin-myplugin"] } ``` ### Testing New Components The architecture is designed to be testable: ```javascript // Test your enhancer const mockBaseHandler = { /* ... */ }; const enhancer = require('./my-authz-plugin')(log, loga, argv, mockBaseHandler); // Test enhanced authorization const req = { /* mock request */ }; const result = enhancer.isAuthorized(req, () => true); ``` ### Potential Future Enhancements Additional authorization enhancers could include: - **`wiki-plugin-permissions`**: Fine-grained page/action permissions - **`wiki-plugin-audit`**: Request logging and audit trails - **`wiki-plugin-ip-filter`**: IP-based access control - **`wiki-plugin-session-mgmt`**: Advanced session management - **`wiki-plugin-2fa`**: Two-factor authentication - **`wiki-plugin-delegation`**: Temporary access delegation - **`wiki-plugin-webhooks`**: Webhook authentication ## Miscellaneous ### TODOs - [x] Make `wiki-security-composable` use regular plugins instead of `wiki-authz` plugins for authorization enhancers. - [x] Created `wiki-plugin-useraccesstokens` as replacement for `wiki-authz-useraccesstokens`. - [x] Created `wiki-plugin-ratelimit` as replacement for `wiki-authz-ratelimit`. - [ ] Manually test by running the wiki server with different configurations. - [x] Test by itself. (Must fail if no auth provider is specified) - [x] Test with `wiki-security-friends`. - [x] Test with `wiki-security-passportjs`. - [x] Test with `wiki-security-friends` and `wiki-plugin-useraccesstokens`. - [x] Test with `wiki-security-passportjs` and `wiki-plugin-useraccesstokens`. - [x] Test with `wiki-security-passportjs`, `wiki-plugin-useraccesstokens`, and `wiki-plugin-ratelimit`. - [x] Make sure that everything in `wiki-plugin-useraccesstokens` has been implemented. - [x] Add a README for `wiki-plugin-ratelimit`. - [x] Make config for `wiki-plugin-ratelimit` flat like the others. - [x] Provide explicit documentation on what the security handler interface does and how to use it, as documentation is lacking in `wiki-server`. ### Security Interface Method Usage Examples The following examples show how each security interface method is used throughout the wiki-server codebase: #### Owner Management **`retrieveOwner(cb)` - Server Startup** ```javascript // wiki-server/lib/server.js:920 // Called during server initialization to load owner information securityhandler.retrieveOwner(e => { // Throw if you can't find the initial owner if (e) throw e owner = securityhandler.getOwner() console.log('owner: ' + owner) app.emit('owner-set') }) ``` **`getOwner()` - Get Current Owner** ```javascript // wiki-server/lib/server.js:923 // Used immediately after retrieveOwner to get the owner value owner = securityhandler.getOwner() ``` **`setOwner(id, cb)` - Site Claiming** ```javascript // wiki-security-friends/server/friends.coffee:109 // Called when a user claims an unclaimed site setOwner id, (err) -> if err console.log 'Failed to claim wiki ', req.hostname, 'error ', err res.sendStatus(500) updateOwner getOwner ``` #### Authentication (User Identification) **`getUser(req)` - Template Data** ```javascript // wiki-server/lib/server.js:329, 365 // Used in route handlers to identify current user for template rendering user = securityhandler.getUser(req) const info = { title, pages: [], authenticated: user ? true : false, user: user, seedNeighbors: argv.neighbors, owned: owner ? true : false, isOwner: securityhandler.isAuthorized(req) ? true : false, ownedBy: owner ? owner : '', } ``` #### Authorization (Access Control) **`isAuthorized(req)` - Write Protection Middleware** ```javascript // wiki-server/lib/server.js:474-480 // Middleware function to protect write operations const authorized = (req, res, next) => { if (securityhandler.isAuthorized(req)) { next() } else { console.log('rejecting', req.path) res.sendStatus(403) } } ``` **`isAuthorized(req)` - Applied to Protected Routes** ```javascript // Examples from wiki-server/lib/server.js app.post('/favicon.png', authorized, (req, res) => { /* ... */ }) app.get('/recycler/favicon.png', authorized, (req, res) => { /* ... */ }) app.get('/recycler/system/slugs.json', authorized, (req, res) => { /* ... */ }) app.get(/^\/recycler\/([a-z0-9-]+)\.json$/, authorized, (req, res) => { /* ... */ }) app.delete(/^\/recycler\/([a-z0-9-]+)\.json$/, authorized, (req, res) => { /* ... */ }) app.get('/proxy/*', authorized, (req, res) => { /* ... */ }) app.put(/^\/page\/([a-z0-9-]+)\/action$/i, authorized, (req, res) => { /* ... */ }) app.delete(/^\/([a-z0-9-]+)\.json$/, authorized, (req, res) => { /* ... */ }) ``` **`isAdmin(req)` - Admin-Only Access** ```javascript // wiki-server/lib/server.js:684-690 // Middleware function for admin-only operations const admin = (req, res, next) => { if (securityhandler.isAdmin(req)) { next() } else { console.log('rejecting', req.path) res.sendStatus(403) } } // Applied to admin routes app.get('/system/version.json', admin, (req, res) => { // Return system version information }) ``` #### Route Definition **`defineRoutes(app, cors, updateOwner)` - Security Plugin Setup** ```javascript // wiki-server/lib/server.js:268 // Called during server initialization to register security routes securityhandler.defineRoutes(app, cors, updateOwner) ``` **Example Route Definitions in Security Plugins:** ```javascript // wiki-security-friends/server/friends.coffee:143-146 security.defineRoutes = (app, cors, updateOwner) -> app.post '/login', cors, security.login(updateOwner) app.get '/logout', cors, security.logout() app.post '/auth/reclaim/', cors, security.reclaim() ``` #### Usage Pattern Analysis 1. **Startup Sequence**: `retrieveOwner()` → `getOwner()` → `defineRoutes()` 2. **Request Processing**: `getUser()` for identification → `isAuthorized()`/`isAdmin()` for access control 3. **Template Rendering**: Both `getUser()` and `isAuthorized()` used to populate template variables 4. **Protection Pattern**: Authorization methods wrapped in middleware functions for route protection This usage pattern demonstrates clear separation between: - **Authentication**: `getUser()` identifies who the user is - **Authorization**: `isAuthorized()`/`isAdmin()` determine what they can do - **Configuration**: Owner management and route definition handle setup ### Client Asset Management One unique aspect of `wiki-security-composable` is how it handles client-side assets. The plugin **copies client assets** from the base authentication provider to its own `client/` directory during initialization. This might seem redundant since both packages are installed as dependencies, but it's necessary due to how the wiki server serves static assets. #### Why Asset Copying is Required The wiki server sets up a **single static route** for security assets: ```javascript // wiki-server/lib/server.js:284 app.use('/security', express.static(path.join(argv.packageDir, argv.security_type, 'client'), staticPathOptions)) ``` This creates a hardcoded expectation that security assets are located at: ``` {packageDir}/{security_type}/client/ ``` When `security_type = "wiki-security-composable"`, the server expects assets at: ``` node_modules/wiki-security-composable/client/ ``` However, the **base authentication provider's assets** (login forms, JavaScript, CSS) are actually located at: ``` node_modules/wiki-security-friends/client/ node_modules/wiki-security-passportjs/client/ ``` #### The Path Resolution Problem Unlike regular plugins, which each get their own static route (`/plugins/pluginname/`), **security plugins share a single `/security/` route**. The server cannot dynamically serve from different packages based on configuration because: 1. **Single Route Limitation**: Only one `app.use('/security', ...)` handler can exist 2. **Package Name Mismatch**: The route path is based on `argv.security_type`, not the base provider's name 3. **Client-Side Expectations**: The base provider's JavaScript expects its assets at `/security/` #### Asset Copying Process During initialization, `wiki-security-composable`: 1. **Detects** the base authentication provider's package directory 2. **Cleans** any existing client assets to avoid conflicts 3. **Copies** all client assets from the base provider to its own `client/` directory 4. **Preserves** the original file structure and permissions ```javascript // Simplified version of the copying logic const baseProviderClientPath = path.join( path.dirname(require.resolve(`${authProvider}/package.json`)), 'client' ); const ourClientPath = path.join(__dirname, '..', 'client'); // Clean and copy fs.rmSync(ourClientPath, { recursive: true, force: true }); copyFiles(baseProviderClientPath, ourClientPath); ``` #### Alternative Approaches Considered This copying approach could be avoided by: 1. **Multiple Static Routes**: Modifying wiki-server to serve from multiple security package paths 2. **Dynamic Route Resolution**: Making `/security/` redirect based on configuration 3. **Symlinks**: Using filesystem symlinks (but cross-platform compatibility issues) 4. **Route Proxying**: Having composable security proxy requests to appropriate packages The **copying approach** was chosen because it: - Maintains **full compatibility** with existing wiki-server code - Provides **asset isolation** and predictable behavior - Works reliably across different deployment scenarios - Allows for **potential asset modification** if needed #### Future Server Integration If this composable architecture were adopted by the core wiki server, the asset copying would become unnecessary. The server could be modified to: 1. **Dynamically resolve** static routes based on the actual authentication provider 2. **Support multiple** security-related static routes 3. **Eliminate** the assumption that `security_type` equals the package containing client assets This represents one of the key areas where the composable architecture has to work around existing server assumptions rather than requiring server modifications.