Add ability for auth plugins to hook tokens validity
This commit is contained in:
		
							parent
							
								
									e1c5503114
								
							
						
					
					
						commit
						e307e4fce3
					
				
					 16 changed files with 298 additions and 132 deletions
				
			
		| 
						 | 
				
			
			@ -20,8 +20,7 @@ tokensRouter.post('/token',
 | 
			
		|||
 | 
			
		||||
tokensRouter.post('/revoke-token',
 | 
			
		||||
  authenticate,
 | 
			
		||||
  asyncMiddleware(handleTokenRevocation),
 | 
			
		||||
  tokenSuccess
 | 
			
		||||
  asyncMiddleware(handleTokenRevocation)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,8 +7,7 @@ import { signJsonLDObject } from './peertube-crypto'
 | 
			
		|||
import { pageToStartAndCount } from './core-utils'
 | 
			
		||||
import { URL } from 'url'
 | 
			
		||||
import { MActor, MVideoAccountLight } from '../typings/models'
 | 
			
		||||
 | 
			
		||||
export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
 | 
			
		||||
import { ContextType } from '@shared/models/activitypub/context'
 | 
			
		||||
 | 
			
		||||
function getContextData (type: ContextType) {
 | 
			
		||||
  const context: any[] = [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 + '.')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ export type OAuthTokenInfo = {
 | 
			
		|||
  user: {
 | 
			
		||||
    id: number
 | 
			
		||||
  }
 | 
			
		||||
  token: MOAuthTokenUser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ScopeNames {
 | 
			
		||||
| 
						 | 
				
			
			@ -136,33 +137,43 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
 | 
			
		|||
    return clearCacheByToken(token.accessToken)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static loadByRefreshToken (refreshToken: string) {
 | 
			
		||||
    const query = {
 | 
			
		||||
      where: { refreshToken }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return OAuthTokenModel.findOne(query)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getByRefreshTokenAndPopulateClient (refreshToken: string) {
 | 
			
		||||
    const query = {
 | 
			
		||||
      where: {
 | 
			
		||||
        refreshToken: refreshToken
 | 
			
		||||
        refreshToken
 | 
			
		||||
      },
 | 
			
		||||
      include: [ OAuthClientModel ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return OAuthTokenModel.findOne(query)
 | 
			
		||||
      .then(token => {
 | 
			
		||||
        if (!token) return null
 | 
			
		||||
    return OAuthTokenModel.scope(ScopeNames.WITH_USER)
 | 
			
		||||
                          .findOne(query)
 | 
			
		||||
                          .then(token => {
 | 
			
		||||
                            if (!token) return null
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          refreshToken: token.refreshToken,
 | 
			
		||||
          refreshTokenExpiresAt: token.refreshTokenExpiresAt,
 | 
			
		||||
          client: {
 | 
			
		||||
            id: token.oAuthClientId
 | 
			
		||||
          },
 | 
			
		||||
          user: {
 | 
			
		||||
            id: token.userId
 | 
			
		||||
          }
 | 
			
		||||
        } as OAuthTokenInfo
 | 
			
		||||
      })
 | 
			
		||||
      .catch(err => {
 | 
			
		||||
        logger.error('getRefreshToken error.', { err })
 | 
			
		||||
        throw err
 | 
			
		||||
      })
 | 
			
		||||
                            return {
 | 
			
		||||
                              refreshToken: token.refreshToken,
 | 
			
		||||
                              refreshTokenExpiresAt: token.refreshTokenExpiresAt,
 | 
			
		||||
                              client: {
 | 
			
		||||
                                id: token.oAuthClientId
 | 
			
		||||
                              },
 | 
			
		||||
                              user: {
 | 
			
		||||
                                id: token.userId
 | 
			
		||||
                              },
 | 
			
		||||
                              token
 | 
			
		||||
                            } as OAuthTokenInfo
 | 
			
		||||
                          })
 | 
			
		||||
                          .catch(err => {
 | 
			
		||||
                            logger.error('getRefreshToken error.', { err })
 | 
			
		||||
                            throw err
 | 
			
		||||
                          })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> {
 | 
			
		||||
| 
						 | 
				
			
			@ -184,14 +195,14 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
 | 
			
		|||
  static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> {
 | 
			
		||||
    const query = {
 | 
			
		||||
      where: {
 | 
			
		||||
        refreshToken: refreshToken
 | 
			
		||||
        refreshToken
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return OAuthTokenModel.scope(ScopeNames.WITH_USER)
 | 
			
		||||
      .findOne(query)
 | 
			
		||||
      .then(token => {
 | 
			
		||||
        if (!token) return new OAuthTokenModel()
 | 
			
		||||
        if (!token) return undefined
 | 
			
		||||
 | 
			
		||||
        return Object.assign(token, { user: token.User })
 | 
			
		||||
      })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,24 @@ async function register ({
 | 
			
		|||
 | 
			
		||||
    getWeight: () => 30,
 | 
			
		||||
 | 
			
		||||
    hookTokenValidity: (options) => {
 | 
			
		||||
      if (options.type === 'refresh') {
 | 
			
		||||
        return { valid: false }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (options.type === 'access') {
 | 
			
		||||
        const token = options.token
 | 
			
		||||
        const now = new Date()
 | 
			
		||||
        now.setTime(now.getTime() - 5000)
 | 
			
		||||
 | 
			
		||||
        const createdAt = new Date(token.createdAt)
 | 
			
		||||
 | 
			
		||||
        return { valid: createdAt.getTime() >= now.getTime() }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return { valid: true }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    login (body) {
 | 
			
		||||
      if (body.id === 'laguna' && body.password === 'laguna password') {
 | 
			
		||||
        return Promise.resolve({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,14 +10,21 @@ import {
 | 
			
		|||
  setAccessTokensToServers,
 | 
			
		||||
  uninstallPlugin,
 | 
			
		||||
  updateMyUser,
 | 
			
		||||
  userLogin
 | 
			
		||||
  userLogin,
 | 
			
		||||
  wait,
 | 
			
		||||
  login, refreshToken
 | 
			
		||||
} from '../../../shared/extra-utils'
 | 
			
		||||
import { User, UserRole } from '@shared/models'
 | 
			
		||||
import { expect } from 'chai'
 | 
			
		||||
 | 
			
		||||
describe('Test id and pass auth plugins', function () {
 | 
			
		||||
  let server: ServerInfo
 | 
			
		||||
  let crashToken: string
 | 
			
		||||
 | 
			
		||||
  let crashAccessToken: string
 | 
			
		||||
  let crashRefreshToken: string
 | 
			
		||||
 | 
			
		||||
  let lagunaAccessToken: string
 | 
			
		||||
  let lagunaRefreshToken: string
 | 
			
		||||
 | 
			
		||||
  before(async function () {
 | 
			
		||||
    this.timeout(30000)
 | 
			
		||||
| 
						 | 
				
			
			@ -50,36 +57,64 @@ describe('Test id and pass auth plugins', function () {
 | 
			
		|||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should login Crash, create the user and use the token', async function () {
 | 
			
		||||
    crashToken = await userLogin(server, { username: 'crash', password: 'crash password' })
 | 
			
		||||
    {
 | 
			
		||||
      const res = await login(server.url, server.client, { username: 'crash', password: 'crash password' })
 | 
			
		||||
      crashAccessToken = res.body.access_token
 | 
			
		||||
      crashRefreshToken = res.body.refresh_token
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const res = await getMyUserInformation(server.url, crashToken)
 | 
			
		||||
    {
 | 
			
		||||
      const res = await getMyUserInformation(server.url, crashAccessToken)
 | 
			
		||||
 | 
			
		||||
    const body: User = res.body
 | 
			
		||||
    expect(body.username).to.equal('crash')
 | 
			
		||||
    expect(body.account.displayName).to.equal('Crash Bandicoot')
 | 
			
		||||
    expect(body.role).to.equal(UserRole.MODERATOR)
 | 
			
		||||
      const body: User = res.body
 | 
			
		||||
      expect(body.username).to.equal('crash')
 | 
			
		||||
      expect(body.account.displayName).to.equal('Crash Bandicoot')
 | 
			
		||||
      expect(body.role).to.equal(UserRole.MODERATOR)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should login the first Laguna, create the user and use the token', async function () {
 | 
			
		||||
    const accessToken = await userLogin(server, { username: 'laguna', password: 'laguna password' })
 | 
			
		||||
    {
 | 
			
		||||
      const res = await login(server.url, server.client, { username: 'laguna', password: 'laguna password' })
 | 
			
		||||
      lagunaAccessToken = res.body.access_token
 | 
			
		||||
      lagunaRefreshToken = res.body.refresh_token
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const res = await getMyUserInformation(server.url, accessToken)
 | 
			
		||||
    {
 | 
			
		||||
      const res = await getMyUserInformation(server.url, lagunaAccessToken)
 | 
			
		||||
 | 
			
		||||
    const body: User = res.body
 | 
			
		||||
    expect(body.username).to.equal('laguna')
 | 
			
		||||
    expect(body.account.displayName).to.equal('laguna')
 | 
			
		||||
    expect(body.role).to.equal(UserRole.USER)
 | 
			
		||||
      const body: User = res.body
 | 
			
		||||
      expect(body.username).to.equal('laguna')
 | 
			
		||||
      expect(body.account.displayName).to.equal('laguna')
 | 
			
		||||
      expect(body.role).to.equal(UserRole.USER)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should refresh crash token, but not laguna token', async function () {
 | 
			
		||||
    {
 | 
			
		||||
      const resRefresh = await refreshToken(server, crashRefreshToken)
 | 
			
		||||
      crashAccessToken = resRefresh.body.access_token
 | 
			
		||||
      crashRefreshToken = resRefresh.body.refresh_token
 | 
			
		||||
 | 
			
		||||
      const res = await getMyUserInformation(server.url, crashAccessToken)
 | 
			
		||||
      const user: User = res.body
 | 
			
		||||
      expect(user.username).to.equal('crash')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      await refreshToken(server, lagunaRefreshToken, 400)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should update Crash profile', async function () {
 | 
			
		||||
    await updateMyUser({
 | 
			
		||||
      url: server.url,
 | 
			
		||||
      accessToken: crashToken,
 | 
			
		||||
      accessToken: crashAccessToken,
 | 
			
		||||
      displayName: 'Beautiful Crash',
 | 
			
		||||
      description: 'Mutant eastern barred bandicoot'
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const res = await getMyUserInformation(server.url, crashToken)
 | 
			
		||||
    const res = await getMyUserInformation(server.url, crashAccessToken)
 | 
			
		||||
 | 
			
		||||
    const body: User = res.body
 | 
			
		||||
    expect(body.account.displayName).to.equal('Beautiful Crash')
 | 
			
		||||
| 
						 | 
				
			
			@ -87,19 +122,19 @@ describe('Test id and pass auth plugins', function () {
 | 
			
		|||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should logout Crash', async function () {
 | 
			
		||||
    await logout(server.url, crashToken)
 | 
			
		||||
    await logout(server.url, crashAccessToken)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should have logged out Crash', async function () {
 | 
			
		||||
    await getMyUserInformation(server.url, crashToken, 401)
 | 
			
		||||
 | 
			
		||||
    await waitUntilLog(server, 'On logout for auth 1 - 2')
 | 
			
		||||
 | 
			
		||||
    await getMyUserInformation(server.url, crashAccessToken, 401)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should login Crash and keep the old existing profile', async function () {
 | 
			
		||||
    crashToken = await userLogin(server, { username: 'crash', password: 'crash password' })
 | 
			
		||||
    crashAccessToken = await userLogin(server, { username: 'crash', password: 'crash password' })
 | 
			
		||||
 | 
			
		||||
    const res = await getMyUserInformation(server.url, crashToken)
 | 
			
		||||
    const res = await getMyUserInformation(server.url, crashAccessToken)
 | 
			
		||||
 | 
			
		||||
    const body: User = res.body
 | 
			
		||||
    expect(body.username).to.equal('crash')
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +143,14 @@ describe('Test id and pass auth plugins', function () {
 | 
			
		|||
    expect(body.role).to.equal(UserRole.MODERATOR)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should correctly auth token of laguna', async function () {
 | 
			
		||||
    this.timeout(10000)
 | 
			
		||||
 | 
			
		||||
    await wait(5000)
 | 
			
		||||
 | 
			
		||||
    await getMyUserInformation(server.url, lagunaAccessToken, 401)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('Should uninstall the plugin one and do not login existing Crash', async function () {
 | 
			
		||||
    await uninstallPlugin({
 | 
			
		||||
      url: server.url,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,6 +46,8 @@ declare module 'express' {
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      refreshTokenAuthName?: string
 | 
			
		||||
 | 
			
		||||
      explicitLogout: boolean
 | 
			
		||||
 | 
			
		||||
      videoAll?: MVideoFullLight
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,24 @@ async function serverLogin (server: Server) {
 | 
			
		|||
  return res.body.access_token as string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function refreshToken (server: ServerInfo, refreshToken: string, expectedStatus = 200) {
 | 
			
		||||
  const path = '/api/v1/users/token'
 | 
			
		||||
 | 
			
		||||
  const body = {
 | 
			
		||||
    client_id: server.client.id,
 | 
			
		||||
    client_secret: server.client.secret,
 | 
			
		||||
    refresh_token: refreshToken,
 | 
			
		||||
    response_type: 'code',
 | 
			
		||||
    grant_type: 'refresh_token'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return request(server.url)
 | 
			
		||||
    .post(path)
 | 
			
		||||
    .type('form')
 | 
			
		||||
    .send(body)
 | 
			
		||||
    .expect(expectedStatus)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function userLogin (server: Server, user: User, expectedStatus = 200) {
 | 
			
		||||
  const res = await login(server.url, server.client, user, expectedStatus)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +101,7 @@ export {
 | 
			
		|||
  login,
 | 
			
		||||
  logout,
 | 
			
		||||
  serverLogin,
 | 
			
		||||
  refreshToken,
 | 
			
		||||
  userLogin,
 | 
			
		||||
  getAccessToken,
 | 
			
		||||
  setAccessTokensToServers,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								shared/models/activitypub/context.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								shared/models/activitypub/context.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { UserRole } from '@shared/models'
 | 
			
		||||
import { MOAuthToken } from '@server/typings/models'
 | 
			
		||||
 | 
			
		||||
export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -6,11 +7,16 @@ export interface RegisterServerAuthPassOptions {
 | 
			
		|||
  // Authentication name (a plugin can register multiple auth strategies)
 | 
			
		||||
  authName: string
 | 
			
		||||
 | 
			
		||||
  onLogout?: Function
 | 
			
		||||
  // Called by PeerTube when a user from your plugin logged out
 | 
			
		||||
  onLogout?(): void
 | 
			
		||||
 | 
			
		||||
  // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
 | 
			
		||||
  getWeight(): number
 | 
			
		||||
 | 
			
		||||
  // Your plugin can hook PeerTube access/refresh token validity
 | 
			
		||||
  // So you can control for your plugin the user session lifetime
 | 
			
		||||
  hookTokenValidity?(options: { token: MOAuthToken, type: 'access' | 'refresh' }): Promise<{ valid: boolean }>
 | 
			
		||||
 | 
			
		||||
  // Used by PeerTube to login a user
 | 
			
		||||
  // Returns null if the login failed, or { username, email } on success
 | 
			
		||||
  login(body: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { ContextType } from '@server/helpers/activitypub'
 | 
			
		||||
import { SendEmailOptions } from './emailer.model'
 | 
			
		||||
import { VideoResolution } from '@shared/models'
 | 
			
		||||
import { ContextType } from '../activitypub/context'
 | 
			
		||||
 | 
			
		||||
export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue