From e1c5503114deef954731904695cd40dccfcef555 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 23 Apr 2020 11:36:50 +0200 Subject: Support logout and add id and pass tests --- server/lib/auth.ts | 50 ++++++++++++++++++++++++---- server/lib/job-queue/job-queue.ts | 23 ++++++++----- server/lib/oauth-model.ts | 19 +++++++++-- server/lib/plugins/plugin-manager.ts | 20 +++++++++-- server/lib/plugins/register-helpers-store.ts | 5 +++ 5 files changed, 99 insertions(+), 18 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth.ts b/server/lib/auth.ts index 18d52fa5a..3495571db 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts @@ -5,6 +5,7 @@ import { PluginManager } from '@server/lib/plugins/plugin-manager' import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' import { logger } from '@server/helpers/logger' import { UserRole } from '@shared/models' +import { revokeToken } from '@server/lib/oauth-model' const oAuthServer = new OAuthServer({ useErrorHandler: true, @@ -37,8 +38,9 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response const aWeight = a.registerAuthOptions.getWeight() const bWeight = b.registerAuthOptions.getWeight() + // DESC weight order if (aWeight === bWeight) return 0 - if (aWeight > bWeight) return 1 + if (aWeight < bWeight) return 1 return -1 }) @@ -48,18 +50,24 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response } for (const pluginAuth of pluginAuths) { + const authOptions = pluginAuth.registerAuthOptions + logger.debug( - 'Using auth method of %s to login %s with weight %d.', - pluginAuth.npmName, loginOptions.id, pluginAuth.registerAuthOptions.getWeight() + 'Using auth method %s of plugin %s to login %s with weight %d.', + authOptions.authName, pluginAuth.npmName, loginOptions.id, authOptions.getWeight() ) - const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions) + const loginResult = await authOptions.login(loginOptions) if (loginResult) { - logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id) + logger.info( + 'Login success with auth method %s of plugin %s for %s.', + authOptions.authName, pluginAuth.npmName, loginOptions.id + ) res.locals.bypassLogin = { bypass: true, pluginName: pluginAuth.npmName, + authName: authOptions.authName, user: { username: loginResult.username, email: loginResult.email, @@ -75,12 +83,40 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response 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 + onExternalAuthPlugin, + handleTokenRevocation } // --------------------------------------------------------------------------- @@ -88,6 +124,8 @@ export { 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, diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index d8d64caaf..14e181835 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts @@ -2,9 +2,16 @@ import * as Bull from 'bull' import { ActivitypubFollowPayload, ActivitypubHttpBroadcastPayload, - ActivitypubHttpFetcherPayload, ActivitypubHttpUnicastPayload, EmailPayload, + ActivitypubHttpFetcherPayload, + ActivitypubHttpUnicastPayload, + EmailPayload, JobState, - JobType, RefreshPayload, VideoFileImportPayload, VideoImportPayload, VideoRedundancyPayload, VideoTranscodingPayload + JobType, + RefreshPayload, + VideoFileImportPayload, + VideoImportPayload, + VideoRedundancyPayload, + VideoTranscodingPayload } from '../../../shared/models' import { logger } from '../../helpers/logger' import { Redis } from '../redis' @@ -13,13 +20,13 @@ import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-bro import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' import { processEmail } from './handlers/email' -import { processVideoTranscoding} from './handlers/video-transcoding' +import { processVideoTranscoding } from './handlers/video-transcoding' import { processActivityPubFollow } from './handlers/activitypub-follow' -import { processVideoImport} from './handlers/video-import' +import { processVideoImport } from './handlers/video-import' import { processVideosViews } from './handlers/video-views' -import { refreshAPObject} from './handlers/activitypub-refresher' -import { processVideoFileImport} from './handlers/video-file-import' -import { processVideoRedundancy} from '@server/lib/job-queue/handlers/video-redundancy' +import { refreshAPObject } from './handlers/activitypub-refresher' +import { processVideoFileImport } from './handlers/video-file-import' +import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' type CreateJobArgument = { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | @@ -117,7 +124,7 @@ class JobQueue { createJob (obj: CreateJobArgument): void { this.createJobWithPromise(obj) - .catch(err => logger.error('Cannot create job.', { err, obj })) + .catch(err => logger.error('Cannot create job.', { err, obj })) } createJobWithPromise (obj: CreateJobArgument) { diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index ea4a67802..7a6ed63be 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts @@ -14,6 +14,7 @@ import { MUser } from '@server/typings/models/user/user' import { UserAdminFlag } from '@shared/models/users/user-flag.model' import { createUserAccountAndChannelAndPlaylist } from './user' import { UserRole } from '@shared/models/users/user-role' +import { PluginManager } from '@server/lib/plugins/plugin-manager' type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } @@ -82,7 +83,7 @@ async function getUser (usernameOrEmail: string, password: string) { const obj = res.locals.bypassLogin logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) - let user = await UserModel.loadByEmail(obj.user.username) + let user = await UserModel.loadByEmail(obj.user.email) if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) // This user does not belong to this plugin, skip it @@ -94,7 +95,8 @@ async function getUser (usernameOrEmail: string, password: string) { logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) - if (!user) return null + // If we don't find the user, or if the user belongs to a plugin + if (!user || user.pluginAuth !== null) return null const passwordMatch = await user.isPasswordMatch(password) if (passwordMatch === false) return null @@ -109,8 +111,14 @@ async function getUser (usernameOrEmail: string, password: string) { } async function revokeToken (tokenInfo: TokenInfo) { + const res: express.Response = this.request.res const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) + if (token) { + if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { + PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) + } + clearCacheByToken(token.accessToken) token.destroy() @@ -123,6 +131,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 + logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') const tokenToCreate = { @@ -130,6 +144,7 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User accessTokenExpiresAt: token.accessTokenExpiresAt, refreshToken: token.refreshToken, refreshTokenExpiresAt: token.refreshTokenExpiresAt, + authName, oAuthClientId: client.id, userId: user.id } diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index f78b989f5..9d646b689 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -76,7 +76,7 @@ export class PluginManager implements ServerHook { return this.registeredPlugins[npmName] } - getRegisteredPlugin (name: string) { + getRegisteredPluginByShortName (name: string) { const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) const registered = this.getRegisteredPluginOrTheme(npmName) @@ -85,7 +85,7 @@ export class PluginManager implements ServerHook { return registered } - getRegisteredTheme (name: string) { + getRegisteredThemeByShortName (name: string) { const npmName = PluginModel.buildNpmName(name, PluginType.THEME) const registered = this.getRegisteredPluginOrTheme(npmName) @@ -132,6 +132,22 @@ export class PluginManager implements ServerHook { return this.translations[locale] || {} } + onLogout (npmName: string, authName: string) { + const plugin = this.getRegisteredPluginOrTheme(npmName) + if (!plugin || plugin.type !== PluginType.PLUGIN) return + + const auth = plugin.registerHelpersStore.getIdAndPassAuths() + .find(a => a.authName === authName) + + if (auth.onLogout) { + try { + auth.onLogout() + } catch (err) { + logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) + } + } + } + // ###################### Hooks ###################### async runHook (hookName: ServerHookName, result?: T, params?: any): Promise { diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts index 7e827401f..679ed3650 100644 --- a/server/lib/plugins/register-helpers-store.ts +++ b/server/lib/plugins/register-helpers-store.ts @@ -171,6 +171,11 @@ export class RegisterHelpersStore { private buildRegisterIdAndPassAuth () { return (options: RegisterServerAuthPassOptions) => { + if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') { + logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName) + return + } + this.idAndPassAuths.push(options) } } -- cgit v1.2.3