From 4a8d113b9b57d97ff13ad1608798eabca99643e4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 28 Apr 2020 14:49:03 +0200 Subject: Begin support for external auths --- server/lib/auth.ts | 225 +++++++++++++++++++-------- server/lib/oauth-model.ts | 2 +- server/lib/plugins/register-helpers-store.ts | 42 +++-- 3 files changed, 179 insertions(+), 90 deletions(-) (limited to 'server/lib') diff --git a/server/lib/auth.ts b/server/lib/auth.ts index 5a6dd9dec..eaae5fdf3 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts @@ -1,13 +1,18 @@ -import * as express from 'express' -import { OAUTH_LIFETIME } from '@server/initializers/constants' -import * as OAuthServer from 'express-oauth-server' -import { PluginManager } from '@server/lib/plugins/plugin-manager' -import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' +import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' import { logger } from '@server/helpers/logger' -import { UserRole } from '@shared/models' +import { generateRandomString } from '@server/helpers/utils' +import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants' import { revokeToken } from '@server/lib/oauth-model' +import { PluginManager } from '@server/lib/plugins/plugin-manager' import { OAuthTokenModel } from '@server/models/oauth/oauth-token' -import { isUserUsernameValid, isUserRoleValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users' +import { UserRole } from '@shared/models' +import { + RegisterServerAuthenticatedResult, + RegisterServerAuthPassOptions, + RegisterServerExternalAuthenticatedResult +} from '@shared/models/plugins/register-server-auth.model' +import * as express from 'express' +import * as OAuthServer from 'express-oauth-server' const oAuthServer = new OAuthServer({ useErrorHandler: true, @@ -17,15 +22,28 @@ const oAuthServer = new OAuthServer({ model: require('./oauth-model') }) -function onExternalAuthPlugin (npmName: string, username: string, email: string) { - -} +// Token is the key, expiration date is the value +const authBypassTokens = new Map() 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) + if (grantType === 'password') { + if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) + else await proxifyPasswordGrant(req, res) + } else if (grantType === 'refresh_token') { + await proxifyRefreshGrant(req, res) + } return forwardTokenReq(req, res, next) } @@ -53,31 +71,60 @@ async function handleTokenRevocation (req: express.Request, res: express.Respons return res.sendStatus(200) } -// --------------------------------------------------------------------------- +async function onExternalUserAuthenticated (options: { + npmName: string + authName: string + authResult: RegisterServerExternalAuthenticatedResult +}) { + const { npmName, authName, authResult } = options -export { - oAuthServer, - handleIdAndPassLogin, - onExternalAuthPlugin, - handleTokenRevocation + if (!authResult.req || !authResult.res) { + logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName) + return + } + + if (!isAuthResultValid(npmName, authName, authResult)) return + + const { res } = authResult + + logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) + + const bypassToken = await generateRandomString(32) + const tokenLifetime = 1000 * 60 * 5 // 5 minutes + + const expires = new Date() + expires.setTime(expires.getTime() + tokenLifetime) + + const user = buildUserResult(authResult) + authBypassTokens.set(bypassToken, { + expires, + user, + npmName, + authName + }) + + res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) } // --------------------------------------------------------------------------- -function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) { +export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, 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() + .json({ + error: err.message, + code: err.name + }) } - return next() + if (next) return next() }) } @@ -131,50 +178,96 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response try { const loginResult = await authOptions.login(loginOptions) - if (loginResult) { - logger.info( - 'Login success with auth method %s of plugin %s for %s.', - authName, npmName, loginOptions.id - ) - - if (!isUserUsernameValid(loginResult.username)) { - logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { loginResult }) - continue - } - - if (!loginResult.email) { - logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult }) - continue - } - - // role is optional - if (loginResult.role && !isUserRoleValid(loginResult.role)) { - logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { loginResult }) - continue - } - - // display name is optional - if (loginResult.displayName && !isUserDisplayNameValid(loginResult.displayName)) { - logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { loginResult }) - continue - } - - res.locals.bypassLogin = { - bypass: true, - pluginName: pluginAuth.npmName, - authName: authOptions.authName, - user: { - username: loginResult.username, - email: loginResult.email, - role: loginResult.role || UserRole.USER, - displayName: loginResult.displayName || loginResult.username - } - } - - return + + if (!loginResult) continue + if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue + + logger.info( + 'Login success with auth method %s of plugin %s for %s.', + authName, npmName, loginOptions.id + ) + + res.locals.bypassLogin = { + bypass: true, + pluginName: pluginAuth.npmName, + authName: authOptions.authName, + user: buildUserResult(loginResult) } + + return } catch (err) { logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) } } } + +function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { + const obj = authBypassTokens.get(req.body.externalAuthToken) + if (!obj) { + logger.error('Cannot authenticate user with unknown bypass token') + return res.sendStatus(400) + } + + const { expires, user, authName, npmName } = obj + + const now = new Date() + if (now.getTime() > expires.getTime()) { + logger.error('Cannot authenticate user with an expired bypass token') + return res.sendStatus(400) + } + + if (user.username !== req.body.username) { + logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) + return res.sendStatus(400) + } + + // Bypass oauth library validation + req.body.password = 'fake' + + logger.info( + 'Auth success with external auth method %s of plugin %s for %s.', + authName, npmName, user.email + ) + + res.locals.bypassLogin = { + bypass: true, + pluginName: npmName, + authName: authName, + user + } +} + +function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { + if (!isUserUsernameValid(result.username)) { + logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { result }) + return false + } + + if (!result.email) { + logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { result }) + return false + } + + // role is optional + if (result.role && !isUserRoleValid(result.role)) { + logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { result }) + return false + } + + // display name is optional + if (result.displayName && !isUserDisplayNameValid(result.displayName)) { + logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { result }) + return false + } + + return true +} + +function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { + return { + username: pluginResult.username, + email: pluginResult.email, + role: pluginResult.role || UserRole.USER, + displayName: pluginResult.displayName || pluginResult.username + } +} diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 6eb0e4473..8b9975bb4 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts @@ -98,7 +98,7 @@ async function getRefreshToken (refreshToken: string) { return tokenInfo } -async function getUser (usernameOrEmail: string, password: string) { +async function getUser (usernameOrEmail?: string, password?: string) { const res: express.Response = this.request.res // Special treatment coming from a plugin diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts index 687974ccf..277f2b687 100644 --- a/server/lib/plugins/register-helpers-store.ts +++ b/server/lib/plugins/register-helpers-store.ts @@ -1,31 +1,21 @@ -import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model' +import { logger } from '@server/helpers/logger' +import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PRIVACIES } from '@server/initializers/constants' +import { onExternalUserAuthenticated } from '@server/lib/auth' import { PluginModel } from '@server/models/server/plugin' +import { RegisterServerOptions } from '@server/typings/plugins' +import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model' +import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model' import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model' +import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model' import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model' -import { - VIDEO_CATEGORIES, - VIDEO_LANGUAGES, - VIDEO_LICENCES, - VIDEO_PLAYLIST_PRIVACIES, - VIDEO_PRIVACIES -} from '@server/initializers/constants' import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model' -import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model' -import { RegisterServerOptions } from '@server/typings/plugins' -import { buildPluginHelpers } from './plugin-helpers' -import { logger } from '@server/helpers/logger' +import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' +import { RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@shared/models/plugins/register-server-auth.model' import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' -import { serverHookObject } from '@shared/models/plugins/server-hook.model' import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' +import { serverHookObject } from '@shared/models/plugins/server-hook.model' import * as express from 'express' -import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' -import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model' -import { - RegisterServerAuthExternalOptions, - RegisterServerAuthExternalResult, - RegisterServerAuthPassOptions -} from '@shared/models/plugins/register-server-auth.model' -import { onExternalAuthPlugin } from '@server/lib/auth' +import { buildPluginHelpers } from './plugin-helpers' type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' type VideoConstant = { [key in number | string]: string } @@ -187,8 +177,14 @@ export class RegisterHelpersStore { this.externalAuths.push(options) return { - onAuth (options: { username: string, email: string }): void { - onExternalAuthPlugin(self.npmName, options.username, options.email) + userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void { + onExternalUserAuthenticated({ + npmName: self.npmName, + authName: options.authName, + authResult: result + }).catch(err => { + logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err }) + }) } } as RegisterServerAuthExternalResult } -- cgit v1.2.3