X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fauth.ts;h=3f8e186334bef7d6b01eb79b6a015ab6cad7bfe7;hb=371906639ee9b6ea4daae504bc7c2b15856c3f38;hp=18d52fa5a7ff5f5623f9a2dbf43a0f03007ed03e;hpb=7fed637506043e4432cbebe041ada0625171cceb;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/auth.ts b/server/lib/auth.ts index 18d52fa5a..3f8e18633 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts @@ -1,10 +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 { generateRandomString } from '@server/helpers/utils' +import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } 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 { UserRole } from '@shared/models' +import { + RegisterServerAuthenticatedResult, + RegisterServerAuthPassOptions, + RegisterServerExternalAuthenticatedResult +} from '@server/types/plugins/register-server-auth.model' +import * as express from 'express' +import * as OAuthServer from 'express-oauth-server' const oAuthServer = new OAuthServer({ useErrorHandler: true, @@ -14,11 +22,131 @@ 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 handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { + const grantType = req.body.grant_type + + 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) +} + +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.json() +} + +async function onExternalUserAuthenticated (options: { + npmName: string + authName: string + authResult: RegisterServerExternalAuthenticatedResult +}) { + const { npmName, authName, authResult } = options + + 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 + } + + const { res } = authResult + + if (!isAuthResultValid(npmName, authName, authResult)) { + res.redirect('/login?externalAuthError=true') + return + } + + logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) + + const bypassToken = await generateRandomString(32) + + const expires = new Date() + expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME) + + const user = buildUserResult(authResult) + authBypassTokens.set(bypassToken, { + expires, + user, + npmName, + authName + }) + + // Cleanup + const now = new Date() + for (const [ key, value ] of authBypassTokens) { + if (value.expires.getTime() < now.getTime()) { + authBypassTokens.delete(key) + } + } + + res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) +} + +// --------------------------------------------------------------------------- + +export { oAuthServer, handleLogin, 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 + }) + } + if (next) return next() + }) } -async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { +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 }[] = [] @@ -37,8 +165,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,54 +177,110 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response } for (const pluginAuth of pluginAuths) { + const authOptions = pluginAuth.registerAuthOptions + const authName = authOptions.authName + const npmName = pluginAuth.npmName + 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.', + authName, npmName, loginOptions.id, authOptions.getWeight() ) - const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions) - if (loginResult) { - logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id) + try { + const loginResult = await authOptions.login(loginOptions) + + 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, - user: { - username: loginResult.username, - email: loginResult.email, - role: loginResult.role || UserRole.USER, - displayName: loginResult.displayName || loginResult.username - } + authName: authOptions.authName, + user: buildUserResult(loginResult) } - break + return + } catch (err) { + logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) } } - - return localLogin(req, res, next) } -// --------------------------------------------------------------------------- +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 external auth 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) + } -export { - oAuthServer, - handleIdAndPassLogin, - onExternalAuthPlugin + // 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, { username: result.username }) + return false + } -function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) { - return oAuthServer.token()(req, res, err => { - if (err) { - return res.status(err.status) - .json({ - error: err.message, - code: err.name - }) - .end() - } + if (!result.email) { + logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) + return false + } - return next() - }) + // 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, { role: result.role }) + 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, { displayName: result.displayName } + ) + 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 + } }