From 9ac85358326f28f47fa3e9e559ff29e2a849952e Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 14 Jun 2024 13:09:20 -0400 Subject: [PATCH] Add support for optional auth --- packages/backend/src/middleware/auth2.js | 108 +---------------- .../src/middleware/configurable_auth.js | 114 ++++++++++++++++++ 2 files changed, 116 insertions(+), 106 deletions(-) create mode 100644 packages/backend/src/middleware/configurable_auth.js diff --git a/packages/backend/src/middleware/auth2.js b/packages/backend/src/middleware/auth2.js index 7a5e8024d..3a7bceb33 100644 --- a/packages/backend/src/middleware/auth2.js +++ b/packages/backend/src/middleware/auth2.js @@ -16,113 +16,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -const APIError = require("../api/APIError"); const config = require("../config"); -const { UserActorType } = require("../services/auth/Actor"); -const { LegacyTokenError } = require("../services/auth/AuthService"); -const { Context } = require("../util/context"); +const configurable_auth = require("./configurable_auth"); -// The "/whoami" endpoint is a special case where we want to allow -// a legacy token to be used for authentication. The "/whoami" -// endpoint will then return a new token for further requests. -// -const is_whoami = (req) => { - if ( ! config.legacy_token_migrate ) return; - - if ( req.path !== '/whoami' ) return; - - // const subdomain = req.subdomains[res.subdomains.length - 1]; - // if ( subdomain !== 'api' ) return; - return true; -} - -// TODO: Allow auth middleware to be used without requiring -// authentication. This will allow us to use the auth middleware -// in endpoints that do not require authentication, but can -// provide additional functionality if the user is authenticated. -const auth2 = async (req, res, next) => { - - // === Getting the Token === - // This step came from jwt_auth in src/helpers.js - // However, since request-response handling is a concern of the - // auth middleware, it makes more sense to put it here. - - let token; - // HTTML Auth header - if(req.header && req.header('Authorization')) - token = req.header('Authorization'); - // Cookie - else if(req.cookies && req.cookies[config.cookie_name]) - token = req.cookies[config.cookie_name]; - // Auth token in URL - else if(req.query && req.query.auth_token) - token = req.query.auth_token; - // Socket - else if(req.handshake && req.handshake.query && req.handshake.query.auth_token) - token = req.handshake.query.auth_token; - - if(!token) { - APIError.create('token_missing').write(res); - return; - } else if (typeof token !== 'string') { - APIError.create('token_auth_failed').write(res); - return; - } else { - token = token.replace('Bearer ', '') - } - - // === Delegate to AuthService === - // AuthService will attempt to authenticate the token and return - // an Actor object, which is a high-level representation of the - // entity that is making the request; it could be a user, an app - // acting on behalf of a user, or an app acting on behalf of itself. - - const context = Context.get(); - const services = context.get('services'); - const svc_auth = services.get('auth'); - - let actor; try { - actor = await svc_auth.authenticate_from_token(token); - } catch ( e ) { - if ( e instanceof APIError ) { - e.write(res); - return; - } - if ( e instanceof LegacyTokenError && is_whoami(req) ) { - const new_info = await svc_auth.check_session(token, { - req, - from_upgrade: true, - }) - context.set('actor', new_info.actor); - context.set('user', new_info.user); - req.new_token = new_info.token; - req.token = new_info.token; - req.user = new_info.user; - req.actor = new_info.actor; - - res.cookie(config.cookie_name, new_info.token, { - sameSite: 'none', - secure: true, - httpOnly: true, - }); - next(); - return; - } - const re = APIError.create('token_auth_failed'); - re.write(res); - return; - } - - // === Populate Context === - context.set('actor', actor); - if ( actor.type.user ) context.set('user', actor.type.user); - - // === Populate Request === - req.actor = actor; - req.user = actor.type.user; - req.token = token; - - next(); -}; +const auth2 = configurable_auth({ optional: false }); module.exports = auth2; diff --git a/packages/backend/src/middleware/configurable_auth.js b/packages/backend/src/middleware/configurable_auth.js new file mode 100644 index 000000000..2f322decb --- /dev/null +++ b/packages/backend/src/middleware/configurable_auth.js @@ -0,0 +1,114 @@ +const APIError = require('../api/APIError'); +const config = require("../config"); +const { LegacyTokenError } = require("../services/auth/AuthService"); +const { Context } = require("../util/context"); + +// The "/whoami" endpoint is a special case where we want to allow +// a legacy token to be used for authentication. The "/whoami" +// endpoint will then return a new token for further requests. +// +const is_whoami = (req) => { + if ( ! config.legacy_token_migrate ) return; + + if ( req.path !== '/whoami' ) return; + + // const subdomain = req.subdomains[res.subdomains.length - 1]; + // if ( subdomain !== 'api' ) return; + return true; +} + +// TODO: Allow auth middleware to be used without requiring +// authentication. This will allow us to use the auth middleware +// in endpoints that do not require authentication, but can +// provide additional functionality if the user is authenticated. +const configurable_auth = options => async (req, res, next) => { + const optional = options?.optional; + + // === Getting the Token === + // This step came from jwt_auth in src/helpers.js + // However, since request-response handling is a concern of the + // auth middleware, it makes more sense to put it here. + + let token; + // HTTML Auth header + if(req.header && req.header('Authorization')) + token = req.header('Authorization'); + // Cookie + else if(req.cookies && req.cookies[config.cookie_name]) + token = req.cookies[config.cookie_name]; + // Auth token in URL + else if(req.query && req.query.auth_token) + token = req.query.auth_token; + // Socket + else if(req.handshake && req.handshake.query && req.handshake.query.auth_token) + token = req.handshake.query.auth_token; + + if(!token) { + if ( optional ) { + next(); + return; + } + APIError.create('token_missing').write(res); + return; + } else if (typeof token !== 'string') { + APIError.create('token_auth_failed').write(res); + return; + } else { + token = token.replace('Bearer ', '') + } + + // === Delegate to AuthService === + // AuthService will attempt to authenticate the token and return + // an Actor object, which is a high-level representation of the + // entity that is making the request; it could be a user, an app + // acting on behalf of a user, or an app acting on behalf of itself. + + const context = Context.get(); + const services = context.get('services'); + const svc_auth = services.get('auth'); + + let actor; try { + actor = await svc_auth.authenticate_from_token(token); + } catch ( e ) { + if ( e instanceof APIError ) { + e.write(res); + return; + } + if ( e instanceof LegacyTokenError && is_whoami(req) ) { + const new_info = await svc_auth.check_session(token, { + req, + from_upgrade: true, + }) + context.set('actor', new_info.actor); + context.set('user', new_info.user); + req.new_token = new_info.token; + req.token = new_info.token; + req.user = new_info.user; + req.actor = new_info.actor; + + res.cookie(config.cookie_name, new_info.token, { + sameSite: 'none', + secure: true, + httpOnly: true, + }); + next(); + return; + } + const re = APIError.create('token_auth_failed'); + re.write(res); + return; + } + + // === Populate Context === + context.set('actor', actor); + if ( actor.type.user ) context.set('user', actor.type.user); + + // === Populate Request === + req.actor = actor; + req.user = actor.type.user; + req.token = token; + + next(); +}; + +module.exports = configurable_auth; \ No newline at end of file