diff --git a/settings.json.docker b/settings.json.docker index 621c7b9b..dcfcef90 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -171,6 +171,13 @@ */ "showSettingsInAdminPage": "${SHOW_SETTINGS_IN_ADMIN_PAGE:true}", + /* + The authentication method used by the server. + The default value is sso + If you want to use the old authentication system, change this to apikey + */ + "authenticationMethod": "${AUTHENTICATION_METHOD:sso}", + /* * Node native SSL support * diff --git a/settings.json.template b/settings.json.template index 039fa296..9e58bad6 100644 --- a/settings.json.template +++ b/settings.json.template @@ -586,6 +586,13 @@ */ "importMaxFileSize": 52428800, // 50 * 1024 * 1024 + /* + The authentication method used by the server. + The default value is sso + If you want to use the old authentication system, change this to apikey + */ + "authenticationMethod": "${AUTHENTICATION_METHOD:sso}", + /* * From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited * diff --git a/src/node/handler/APIHandler.ts b/src/node/handler/APIHandler.ts index 17346b79..a0b90bb1 100644 --- a/src/node/handler/APIHandler.ts +++ b/src/node/handler/APIHandler.ts @@ -27,7 +27,7 @@ import createHTTPError from 'http-errors'; import {Http2ServerRequest, Http2ServerResponse} from "node:http2"; import {publicKeyExported} from "../security/OAuth2Provider"; import {jwtVerify} from "jose"; - +import {apikey} from './APIKeyHandler' // a list of all functions const version:MapArrayType = {}; @@ -149,6 +149,7 @@ exports.version = version; type APIFields = { + apikey: string; api_key: string; padID: string; padName: string; @@ -160,9 +161,9 @@ type APIFields = { * @param {String} functionName the name of the called function * @param fields the params of the called function * @param req express request object - * @param res express response object */ -exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields, req: Http2ServerRequest, res: Http2ServerResponse) { +exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields, + req: Http2ServerRequest) { // say goodbye if this is an unknown API version if (!(apiVersion in version)) { throw new createHTTPError.NotFound('no such api version'); @@ -177,16 +178,21 @@ exports.handle = async function (apiVersion: string, functionName: string, field throw new createHTTPError.Unauthorized('no or wrong API Key'); } - try { - await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'], - requiredClaims: ["admin"]}) - - } catch (e) { - throw new createHTTPError.Unauthorized('no or wrong API Key'); + if (apikey !== null && apikey.trim().length > 0) { + fields.apikey = fields.apikey || fields.api_key; + // API key is configured, check if it is valid + if (fields.apikey !== apikey!.trim()) { + throw new createHTTPError.Unauthorized('no or wrong API Key'); + } + } else { + try { + await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'], + requiredClaims: ["admin"]}) + } catch (e) { + throw new createHTTPError.Unauthorized('no or wrong OAuth token'); + } } - - // sanitize any padIDs before continuing if (fields.padID) { fields.padID = await padManager.sanitizePadId(fields.padID); diff --git a/src/node/handler/APIKeyHandler.ts b/src/node/handler/APIKeyHandler.ts new file mode 100644 index 00000000..b4e70f6e --- /dev/null +++ b/src/node/handler/APIKeyHandler.ts @@ -0,0 +1,25 @@ +const absolutePaths = require('../utils/AbsolutePaths'); +import fs from 'fs'; +import log4js from 'log4js'; +const randomString = require('../utils/randomstring'); +const argv = require('../utils/Cli').argv; +const settings = require('../utils/Settings'); + +const apiHandlerLogger = log4js.getLogger('APIHandler'); + +// ensure we have an apikey +export let apikey:string|null = null; +const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); + + +if(settings.authenticationMethod === 'apikey') { + try { + apikey = fs.readFileSync(apikeyFilename, 'utf8'); + apiHandlerLogger.info(`Api key file read from: "${apikeyFilename}"`); + } catch (e) { + apiHandlerLogger.info( + `Api key file "${apikeyFilename}" not found. Creating with random contents.`); + apikey = randomString(32); + fs.writeFileSync(apikeyFilename, apikey!, 'utf8'); + } +} diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index a55e6787..8b04adf9 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -608,7 +608,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { for (const funcName of Object.keys(apiHandler.version[version])) { const handler = async (c: any, req:any, res:any) => { // parse fields from request - const {header, params, query} = c.request; + const {headers, params, query} = c.request; // read form data if method was POST let formData:MapArrayType = {}; @@ -622,8 +622,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { } } - const fields = Object.assign({}, header, params, query, formData); - + const fields = Object.assign({}, headers, params, query, formData); if (logger.isDebugEnabled()) { logger.debug(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); } diff --git a/src/node/utils/Cli.ts b/src/node/utils/Cli.ts index 1b293819..1579dd3c 100644 --- a/src/node/utils/Cli.ts +++ b/src/node/utils/Cli.ts @@ -45,5 +45,10 @@ for (let i = 0; i < argv.length; i++) { exports.argv.sessionkey = arg; } + // Override location of APIKEY.txt file + if (prevArg === '--apikey') { + exports.argv.apikey = arg; + } + prevArg = arg; } diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index 1e8485c0..ff9326c5 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -156,6 +156,15 @@ exports.socketIo = { maxHttpBufferSize: 10000, }; + +/* + The authentication method used by the server. + The default value is sso + If you want to use the old authentication system, change this to apikey + */ +exports.authenticationMethod = 'sso' + + /* * The Type of the database */ @@ -519,6 +528,8 @@ exports.getGitCommit = () => { // Return etherpad version from package.json exports.getEpVersion = () => require('../../package.json').version; + + /** * Receives a settingsObj and, if the property name is a valid configuration * item, stores it in the module's exported properties via a side effect.