Add ability for auth plugins to hook tokens validity
authorChocobozzz <me@florianbigard.com>
Fri, 24 Apr 2020 09:33:01 +0000 (11:33 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 4 May 2020 14:21:39 +0000 (16:21 +0200)
16 files changed:
server/controllers/api/users/token.ts
server/helpers/activitypub.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/utils.ts
server/lib/auth.ts
server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
server/lib/oauth-model.ts
server/lib/plugins/plugin-manager.ts
server/models/oauth/oauth-token.ts
server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
server/tests/plugins/id-and-pass-auth.ts
server/typings/express.ts
shared/extra-utils/users/login.ts
shared/models/activitypub/context.ts [new file with mode: 0644]
shared/models/plugins/register-server-auth.model.ts
shared/models/server/job.model.ts

index 9694f9e5e819a7a1b4c97c69a23b492955428535..f4be228f62f9eb0d84d41112bf6d41396b61aad8 100644 (file)
@@ -20,8 +20,7 @@ tokensRouter.post('/token',
 
 tokensRouter.post('/revoke-token',
   authenticate,
-  asyncMiddleware(handleTokenRevocation),
-  tokenSuccess
+  asyncMiddleware(handleTokenRevocation)
 )
 
 // ---------------------------------------------------------------------------
index 2d49e6869d690adb75b838df41fdbea0c582a2bf..aeb8fde01f5575ea9f9f90ea57351fd58f4d43d9 100644 (file)
@@ -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[] = [
index 0635c7b661198a8989084855aae4528549d505aa..e521cabbcfdcf192f3e74693b6fcbf9c9e08a667 100644 (file)
@@ -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
index 0dfcc51be37eb7936f1afe03705e57aa700455c5..44a8926e52b546dd7d4d59ddf59c684a55d1eacb 100644 (file)
@@ -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
index 3495571dbcbf42ba57a891b9d488fb5425050b51..c2a6fcaffd7e65f1658242a56cb12444b46dce17 100644 (file)
@@ -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()
-  })
 }
index 437ea06fc1779fb952a0cbf07e7d9ac5d5ac901c..bcb49a73189ec20bc106c889afee99d0699d04d6 100644 (file)
@@ -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 }
 
index 7a6ed63be3477d78aaefeef7b74995dc268f48a0..6eb0e447320189343b97b753baa29df9784f906b 100644 (file)
@@ -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 + '.')
 
index 9d646b68976557da9f9d562a6c405be9e06438d9..c64ca60aaebe7a05dd5f1c0c9a712f9acab76dec 100644 (file)
@@ -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) {
index e73c4be7d54a8496e09350448bb665cad639f306..3541b6103811f6a3d6d5a9443a7f84c26e8b8ad4 100644 (file)
@@ -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 {
-          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 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
+                              },
+                              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 })
       })
index c0e560019c59e5951d25a4457b39057126e472a5..ceab7b60de3a96284e0ed688aaac6b9b2da9aebf 100644 (file)
@@ -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({
index 45fa7856ce987bedc75058122b71091183d80e9b..0268d35a037c96f736db70e4b50ca103c8dd1fbc 100644 (file)
@@ -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,
index 2d12a486a6a895c08ad749343ac7df8c661b3f08..e6e120403963466422d164a27d6916cc96b6b942 100644 (file)
@@ -46,6 +46,8 @@ declare module 'express' {
         }
       }
 
+      refreshTokenAuthName?: string
+
       explicitLogout: boolean
 
       videoAll?: MVideoFullLight
index 2d68337a6affd38f32b5243500d9cb8dc37da0d5..b12b51b8c54149f7826c1a481f6f42b8fc8885f4 100644 (file)
@@ -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,
diff --git a/shared/models/activitypub/context.ts b/shared/models/activitypub/context.ts
new file mode 100644 (file)
index 0000000..bd795a2
--- /dev/null
@@ -0,0 +1 @@
+export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
index dc46dcbc804727a45bbf307b4008232d2502a8fb..403a499946bf2c221d11b1c2d556ecac9645923d 100644 (file)
@@ -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: {
index 694361276c23c8bc051dfe9ad2046c4219b26226..57d61c480321643698b9391ff8b02804781f7f8c 100644 (file)
@@ -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'