From e307e4fce39853d445d086f92b8c556c363ee15d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 24 Apr 2020 11:33:01 +0200 Subject: Add ability for auth plugins to hook tokens validity --- server/lib/activitypub/send/send-create.ts | 2 +- server/lib/activitypub/send/utils.ts | 2 +- server/lib/auth.ts | 128 +++++++++++---------- .../handlers/utils/activitypub-http-utils.ts | 3 +- server/lib/oauth-model.ts | 62 +++++++--- server/lib/plugins/plugin-manager.ts | 39 ++++++- 6 files changed, 152 insertions(+), 84 deletions(-) (limited to 'server/lib') diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 0635c7b66..e521cabbc 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -15,8 +15,8 @@ import { MVideoRedundancyFileVideo, MVideoRedundancyStreamingPlaylistVideo } from '../../../typings/models' -import { ContextType } from '@server/helpers/activitypub' import { getServerActor } from '@server/models/application/application' +import { ContextType } from '@shared/models/activitypub/context' async function sendCreateVideo (video: MVideoAP, t: Transaction) { if (!video.hasPrivacyForFederation()) return undefined diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts index 0dfcc51be..44a8926e5 100644 --- a/server/lib/activitypub/send/utils.ts +++ b/server/lib/activitypub/send/utils.ts @@ -7,8 +7,8 @@ import { JobQueue } from '../../job-queue' import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' import { afterCommitIfTransaction } from '../../../helpers/database-utils' import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models' -import { ContextType } from '@server/helpers/activitypub' import { getServerActor } from '@server/models/application/application' +import { ContextType } from '@shared/models/activitypub/context' async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { byActor: MActorLight diff --git a/server/lib/auth.ts b/server/lib/auth.ts index 3495571db..c2a6fcaff 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts @@ -6,6 +6,7 @@ import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-s import { logger } from '@server/helpers/logger' import { UserRole } from '@shared/models' import { revokeToken } from '@server/lib/oauth-model' +import { OAuthTokenModel } from '@server/models/oauth/oauth-token' const oAuthServer = new OAuthServer({ useErrorHandler: true, @@ -20,6 +21,74 @@ function onExternalAuthPlugin (npmName: string, username: string, email: string) } async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { + const grantType = req.body.grant_type + + if (grantType === 'password') await proxifyPasswordGrant(req, res) + else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res) + + return forwardTokenReq(req, res, next) +} + +async function handleTokenRevocation (req: express.Request, res: express.Response) { + const token = res.locals.oauth.token + + res.locals.explicitLogout = true + await revokeToken(token) + + // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released + // oAuthServer.revoke(req, res, err => { + // if (err) { + // logger.warn('Error in revoke token handler.', { err }) + // + // return res.status(err.status) + // .json({ + // error: err.message, + // code: err.name + // }) + // .end() + // } + // }) + + return res.sendStatus(200) +} + +// --------------------------------------------------------------------------- + +export { + oAuthServer, + handleIdAndPassLogin, + onExternalAuthPlugin, + handleTokenRevocation +} + +// --------------------------------------------------------------------------- + +function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) { + return oAuthServer.token()(req, res, err => { + if (err) { + logger.warn('Login error.', { err }) + + return res.status(err.status) + .json({ + error: err.message, + code: err.name + }) + .end() + } + + return next() + }) +} + +async function proxifyRefreshGrant (req: express.Request, res: express.Response) { + const refreshToken = req.body.refresh_token + if (!refreshToken) return + + const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) + if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName +} + +async function proxifyPasswordGrant (req: express.Request, res: express.Response) { const plugins = PluginManager.Instance.getIdAndPassAuths() const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] @@ -76,64 +145,7 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response } } - break + return } } - - return localLogin(req, res, next) -} - -async function handleTokenRevocation (req: express.Request, res: express.Response) { - const token = res.locals.oauth.token - - PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) - - await revokeToken(token) - .catch(err => { - logger.error('Cannot revoke token.', err) - }) - - // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released - // oAuthServer.revoke(req, res, err => { - // if (err) { - // logger.warn('Error in revoke token handler.', { err }) - // - // return res.status(err.status) - // .json({ - // error: err.message, - // code: err.name - // }) - // .end() - // } - // }) - - return res.sendStatus(200) -} - -// --------------------------------------------------------------------------- - -export { - oAuthServer, - handleIdAndPassLogin, - onExternalAuthPlugin, - handleTokenRevocation -} - -// --------------------------------------------------------------------------- - -function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) { - return oAuthServer.token()(req, res, err => { - if (err) { - logger.warn('Login error.', { err }) - - return res.status(err.status) - .json({ - error: err.message, - code: err.name - }) - .end() - } - - return next() - }) } diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index 437ea06fc..bcb49a731 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts @@ -1,9 +1,10 @@ -import { buildSignedActivity, ContextType } from '../../../../helpers/activitypub' +import { buildSignedActivity } from '../../../../helpers/activitypub' import { ActorModel } from '../../../../models/activitypub/actor' import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' import { MActor } from '../../../../typings/models' import { getServerActor } from '@server/models/application/application' import { buildDigest } from '@server/helpers/peertube-crypto' +import { ContextType } from '@shared/models/activitypub/context' type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 7a6ed63be..6eb0e4473 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts @@ -1,4 +1,3 @@ -import * as Bluebird from 'bluebird' import * as express from 'express' import { AccessDeniedError } from 'oauth2-server' import { logger } from '../helpers/logger' @@ -47,22 +46,33 @@ function clearCacheByToken (token: string) { } } -function getAccessToken (bearerToken: string) { +async function getAccessToken (bearerToken: string) { logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') - if (!bearerToken) return Bluebird.resolve(undefined) + if (!bearerToken) return undefined - if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken)) + let tokenModel: MOAuthTokenUser - return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) - .then(tokenModel => { - if (tokenModel) { - accessTokenCache.set(bearerToken, tokenModel) - userHavingToken.set(tokenModel.userId, tokenModel.accessToken) - } + if (accessTokenCache.has(bearerToken)) { + tokenModel = accessTokenCache.get(bearerToken) + } else { + tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) - return tokenModel - }) + if (tokenModel) { + accessTokenCache.set(bearerToken, tokenModel) + userHavingToken.set(tokenModel.userId, tokenModel.accessToken) + } + } + + if (!tokenModel) return undefined + + if (tokenModel.User.pluginAuth) { + const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access') + + if (valid !== true) return undefined + } + + return tokenModel } function getClient (clientId: string, clientSecret: string) { @@ -71,14 +81,27 @@ function getClient (clientId: string, clientSecret: string) { return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) } -function getRefreshToken (refreshToken: string) { +async function getRefreshToken (refreshToken: string) { logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') - return OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) + const tokenInfo = await OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) + if (!tokenInfo) return undefined + + const tokenModel = tokenInfo.token + + if (tokenModel.User.pluginAuth) { + const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh') + + if (valid !== true) return undefined + } + + return tokenInfo } async function getUser (usernameOrEmail: string, password: string) { const res: express.Response = this.request.res + + // Special treatment coming from a plugin if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { const obj = res.locals.bypassLogin logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) @@ -110,7 +133,7 @@ async function getUser (usernameOrEmail: string, password: string) { return user } -async function revokeToken (tokenInfo: TokenInfo) { +async function revokeToken (tokenInfo: { refreshToken: string }) { const res: express.Response = this.request.res const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) @@ -133,9 +156,12 @@ async function revokeToken (tokenInfo: TokenInfo) { async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { const res: express.Response = this.request.res - const authName = res.locals.bypassLogin?.bypass === true - ? res.locals.bypassLogin.authName - : null + let authName: string = null + if (res.locals.bypassLogin?.bypass === true) { + authName = res.locals.bypassLogin.authName + } else if (res.locals.refreshTokenAuthName) { + authName = res.locals.refreshTokenAuthName + } logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 9d646b689..c64ca60aa 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -21,6 +21,7 @@ import { ClientHtml } from '../client-html' import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' import { RegisterHelpersStore } from './register-helpers-store' import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' +import { MOAuthTokenUser } from '@server/typings/models' export interface RegisteredPlugin { npmName: string @@ -133,13 +134,11 @@ export class PluginManager implements ServerHook { } onLogout (npmName: string, authName: string) { - const plugin = this.getRegisteredPluginOrTheme(npmName) - if (!plugin || plugin.type !== PluginType.PLUGIN) return + const auth = this.getAuth(npmName, authName) - const auth = plugin.registerHelpersStore.getIdAndPassAuths() - .find(a => a.authName === authName) + if (auth?.onLogout) { + logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName) - if (auth.onLogout) { try { auth.onLogout() } catch (err) { @@ -148,6 +147,28 @@ export class PluginManager implements ServerHook { } } + async isTokenValid (token: MOAuthTokenUser, type: 'access' | 'refresh') { + const auth = this.getAuth(token.User.pluginAuth, token.authName) + if (!auth) return true + + if (auth.hookTokenValidity) { + try { + const { valid } = await auth.hookTokenValidity({ token, type }) + + if (valid === false) { + logger.info('Rejecting %s token validity from auth %s of plugin %s', type, token.authName, token.User.pluginAuth) + } + + return valid + } catch (err) { + logger.warn('Cannot run check token validity from auth %s of plugin %s.', token.authName, token.User.pluginAuth, { err }) + return true + } + } + + return true + } + // ###################### Hooks ###################### async runHook (hookName: ServerHookName, result?: T, params?: any): Promise { @@ -453,6 +474,14 @@ export class PluginManager implements ServerHook { return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) } + private getAuth (npmName: string, authName: string) { + const plugin = this.getRegisteredPluginOrTheme(npmName) + if (!plugin || plugin.type !== PluginType.PLUGIN) return null + + return plugin.registerHelpersStore.getIdAndPassAuths() + .find(a => a.authName === authName) + } + // ###################### Private getters ###################### private getRegisteredPluginsOrThemes (type: PluginType) { -- cgit v1.2.3