1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-04 10:19:35 +02:00

Refactor auth flow

Reimplement some node-oauth2-server methods to remove hacky code needed by our external
login workflow
This commit is contained in:
Chocobozzz 2021-03-12 15:20:46 +01:00
parent cae2df6bdc
commit f43db2f46e
No known key found for this signature in database
GPG key ID: 583A612D890159BE
24 changed files with 487 additions and 255 deletions

View file

@ -2,8 +2,10 @@ import * as express from 'express'
import * as RateLimit from 'express-rate-limit'
import { tokensRouter } from '@server/controllers/api/users/token'
import { Hooks } from '@server/lib/plugins/hooks'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
import { MUser, MUserAccountDefault } from '@server/types/models'
import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
import { UserRegister } from '../../../../shared/models/users/user-register.model'
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { Emailer } from '../../../lib/emailer'
import { Notifier } from '../../../lib/notifier'
import { deleteUserToken } from '../../../lib/oauth-model'
import { Redis } from '../../../lib/redis'
import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
import {
@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history'
import { myNotificationsRouter } from './my-notifications'
import { mySubscriptionsRouter } from './my-subscriptions'
import { myVideoPlaylistsRouter } from './my-video-playlists'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
const auditLogger = auditLoggerFactory('users')
@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) {
const user = await userToUpdate.save()
// Destroy user token to refresh rights
if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id)
if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault
user.blockedReason = reason || null
await sequelizeTypescript.transaction(async t => {
await deleteUserToken(user.id, t)
await OAuthTokenModel.deleteUserToken(user.id, t)
await user.save({ transaction: t })
})

View file

@ -1,11 +1,14 @@
import { handleLogin, handleTokenRevocation } from '@server/lib/auth'
import * as RateLimit from 'express-rate-limit'
import { CONFIG } from '@server/initializers/config'
import * as express from 'express'
import * as RateLimit from 'express-rate-limit'
import { v4 as uuidv4 } from 'uuid'
import { logger } from '@server/helpers/logger'
import { CONFIG } from '@server/initializers/config'
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
import { handleOAuthToken } from '@server/lib/auth/oauth'
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
import { Hooks } from '@server/lib/plugins/hooks'
import { asyncMiddleware, authenticate } from '@server/middlewares'
import { ScopedToken } from '@shared/models/users/user-scoped-token'
import { v4 as uuidv4 } from 'uuid'
const tokensRouter = express.Router()
@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({
tokensRouter.post('/token',
loginRateLimiter,
handleLogin,
tokenSuccess
asyncMiddleware(handleToken)
)
tokensRouter.post('/revoke-token',
@ -42,10 +44,53 @@ export {
}
// ---------------------------------------------------------------------------
function tokenSuccess (req: express.Request) {
const username = req.body.username
async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
const grantType = req.body.grant_type
Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
try {
const bypassLogin = await buildByPassLogin(req, grantType)
const refreshTokenAuthName = grantType === 'refresh_token'
? await getAuthNameFromRefreshGrant(req.body.refresh_token)
: undefined
const options = {
refreshTokenAuthName,
bypassLogin
}
const token = await handleOAuthToken(req, options)
res.set('Cache-Control', 'no-store')
res.set('Pragma', 'no-cache')
Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip })
return res.json({
token_type: 'Bearer',
access_token: token.accessToken,
refresh_token: token.refreshToken,
expires_in: token.accessTokenExpiresIn,
refresh_token_expires_in: token.refreshTokenExpiresIn
})
} catch (err) {
logger.warn('Login error', { err })
return res.status(err.code || 400).json({
code: err.name,
error: err.message
})
}
}
async function handleTokenRevocation (req: express.Request, res: express.Response) {
const token = res.locals.oauth.token
const result = await revokeToken(token, true)
return res.json(result)
}
function getScopedTokens (req: express.Request, res: express.Response) {
@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) {
feedToken: user.feedToken
} as ScopedToken)
}
async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
if (grantType !== 'password') return undefined
if (req.body.externalAuthToken) {
// Consistency with the getBypassFromPasswordGrant promise
return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
}
return getBypassFromPasswordGrant(req.body.username, req.body.password)
}

View file

@ -1,15 +1,15 @@
import * as express from 'express'
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
import { join } from 'path'
import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins'
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { logger } from '@server/helpers/logger'
import { optionalAuthenticate } from '@server/middlewares/auth'
import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { PluginType } from '../../shared/models/plugins/plugin.type'
import { isTestInstance } from '../helpers/core-utils'
import { logger } from '@server/helpers/logger'
import { optionalAuthenticate } from '@server/middlewares/oauth'
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
const sendFileOptions = {
maxAge: '30 days',