Add ability for auth plugins to hook tokens validity

This commit is contained in:
Chocobozzz 2020-04-24 11:33:01 +02:00 committed by Chocobozzz
parent e1c5503114
commit e307e4fce3
16 changed files with 298 additions and 132 deletions

View File

@ -20,8 +20,7 @@ tokensRouter.post('/token',
tokensRouter.post('/revoke-token', tokensRouter.post('/revoke-token',
authenticate, authenticate,
asyncMiddleware(handleTokenRevocation), asyncMiddleware(handleTokenRevocation)
tokenSuccess
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -7,8 +7,7 @@ import { signJsonLDObject } from './peertube-crypto'
import { pageToStartAndCount } from './core-utils' import { pageToStartAndCount } from './core-utils'
import { URL } from 'url' import { URL } from 'url'
import { MActor, MVideoAccountLight } from '../typings/models' import { MActor, MVideoAccountLight } from '../typings/models'
import { ContextType } from '@shared/models/activitypub/context'
export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
function getContextData (type: ContextType) { function getContextData (type: ContextType) {
const context: any[] = [ const context: any[] = [

View File

@ -15,8 +15,8 @@ import {
MVideoRedundancyFileVideo, MVideoRedundancyFileVideo,
MVideoRedundancyStreamingPlaylistVideo MVideoRedundancyStreamingPlaylistVideo
} from '../../../typings/models' } from '../../../typings/models'
import { ContextType } from '@server/helpers/activitypub'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { ContextType } from '@shared/models/activitypub/context'
async function sendCreateVideo (video: MVideoAP, t: Transaction) { async function sendCreateVideo (video: MVideoAP, t: Transaction) {
if (!video.hasPrivacyForFederation()) return undefined if (!video.hasPrivacyForFederation()) return undefined

View File

@ -7,8 +7,8 @@ import { JobQueue } from '../../job-queue'
import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
import { afterCommitIfTransaction } from '../../../helpers/database-utils' import { afterCommitIfTransaction } from '../../../helpers/database-utils'
import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models' 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 { getServerActor } from '@server/models/application/application'
import { ContextType } from '@shared/models/activitypub/context'
async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
byActor: MActorLight byActor: MActorLight

View File

@ -6,6 +6,7 @@ import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-s
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { UserRole } from '@shared/models' import { UserRole } from '@shared/models'
import { revokeToken } from '@server/lib/oauth-model' import { revokeToken } from '@server/lib/oauth-model'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
const oAuthServer = new OAuthServer({ const oAuthServer = new OAuthServer({
useErrorHandler: true, 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) { 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 plugins = PluginManager.Instance.getIdAndPassAuths()
const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] 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()
})
} }

View File

@ -1,9 +1,10 @@
import { buildSignedActivity, ContextType } from '../../../../helpers/activitypub' import { buildSignedActivity } from '../../../../helpers/activitypub'
import { ActorModel } from '../../../../models/activitypub/actor' import { ActorModel } from '../../../../models/activitypub/actor'
import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants'
import { MActor } from '../../../../typings/models' import { MActor } from '../../../../typings/models'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { buildDigest } from '@server/helpers/peertube-crypto' import { buildDigest } from '@server/helpers/peertube-crypto'
import { ContextType } from '@shared/models/activitypub/context'
type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } type Payload = { body: any, contextType?: ContextType, signatureActorId?: number }

View File

@ -1,4 +1,3 @@
import * as Bluebird from 'bluebird'
import * as express from 'express' import * as express from 'express'
import { AccessDeniedError } from 'oauth2-server' import { AccessDeniedError } from 'oauth2-server'
import { logger } from '../helpers/logger' 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 + ').') 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) if (accessTokenCache.has(bearerToken)) {
.then(tokenModel => { tokenModel = accessTokenCache.get(bearerToken)
if (tokenModel) { } else {
accessTokenCache.set(bearerToken, tokenModel) tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
}
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) { function getClient (clientId: string, clientSecret: string) {
@ -71,14 +81,27 @@ function getClient (clientId: string, clientSecret: string) {
return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) return OAuthClientModel.getByIdAndSecret(clientId, clientSecret)
} }
function getRefreshToken (refreshToken: string) { async function getRefreshToken (refreshToken: string) {
logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') 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) { async function getUser (usernameOrEmail: string, password: string) {
const res: express.Response = this.request.res const res: express.Response = this.request.res
// Special treatment coming from a plugin
if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) {
const obj = res.locals.bypassLogin const obj = res.locals.bypassLogin
logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
@ -110,7 +133,7 @@ async function getUser (usernameOrEmail: string, password: string) {
return user return user
} }
async function revokeToken (tokenInfo: TokenInfo) { async function revokeToken (tokenInfo: { refreshToken: string }) {
const res: express.Response = this.request.res const res: express.Response = this.request.res
const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 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) { async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
const res: express.Response = this.request.res const res: express.Response = this.request.res
const authName = res.locals.bypassLogin?.bypass === true let authName: string = null
? res.locals.bypassLogin.authName if (res.locals.bypassLogin?.bypass === true) {
: null 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 + '.') logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')

View File

@ -21,6 +21,7 @@ import { ClientHtml } from '../client-html'
import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
import { RegisterHelpersStore } from './register-helpers-store' import { RegisterHelpersStore } from './register-helpers-store'
import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
import { MOAuthTokenUser } from '@server/typings/models'
export interface RegisteredPlugin { export interface RegisteredPlugin {
npmName: string npmName: string
@ -133,13 +134,11 @@ export class PluginManager implements ServerHook {
} }
onLogout (npmName: string, authName: string) { onLogout (npmName: string, authName: string) {
const plugin = this.getRegisteredPluginOrTheme(npmName) const auth = this.getAuth(npmName, authName)
if (!plugin || plugin.type !== PluginType.PLUGIN) return
const auth = plugin.registerHelpersStore.getIdAndPassAuths() if (auth?.onLogout) {
.find(a => a.authName === authName) logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
if (auth.onLogout) {
try { try {
auth.onLogout() auth.onLogout()
} catch (err) { } 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 ###################### // ###################### Hooks ######################
async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 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) 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 getters ######################
private getRegisteredPluginsOrThemes (type: PluginType) { private getRegisteredPluginsOrThemes (type: PluginType) {

View File

@ -30,6 +30,7 @@ export type OAuthTokenInfo = {
user: { user: {
id: number id: number
} }
token: MOAuthTokenUser
} }
enum ScopeNames { enum ScopeNames {
@ -136,33 +137,43 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
return clearCacheByToken(token.accessToken) return clearCacheByToken(token.accessToken)
} }
static loadByRefreshToken (refreshToken: string) {
const query = {
where: { refreshToken }
}
return OAuthTokenModel.findOne(query)
}
static getByRefreshTokenAndPopulateClient (refreshToken: string) { static getByRefreshTokenAndPopulateClient (refreshToken: string) {
const query = { const query = {
where: { where: {
refreshToken: refreshToken refreshToken
}, },
include: [ OAuthClientModel ] include: [ OAuthClientModel ]
} }
return OAuthTokenModel.findOne(query) return OAuthTokenModel.scope(ScopeNames.WITH_USER)
.then(token => { .findOne(query)
if (!token) return null .then(token => {
if (!token) return null
return { return {
refreshToken: token.refreshToken, refreshToken: token.refreshToken,
refreshTokenExpiresAt: token.refreshTokenExpiresAt, refreshTokenExpiresAt: token.refreshTokenExpiresAt,
client: { client: {
id: token.oAuthClientId id: token.oAuthClientId
}, },
user: { user: {
id: token.userId id: token.userId
} },
} as OAuthTokenInfo token
}) } as OAuthTokenInfo
.catch(err => { })
logger.error('getRefreshToken error.', { err }) .catch(err => {
throw err logger.error('getRefreshToken error.', { err })
}) throw err
})
} }
static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> { static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> {
@ -184,14 +195,14 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> { static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> {
const query = { const query = {
where: { where: {
refreshToken: refreshToken refreshToken
} }
} }
return OAuthTokenModel.scope(ScopeNames.WITH_USER) return OAuthTokenModel.scope(ScopeNames.WITH_USER)
.findOne(query) .findOne(query)
.then(token => { .then(token => {
if (!token) return new OAuthTokenModel() if (!token) return undefined
return Object.assign(token, { user: token.User }) return Object.assign(token, { user: token.User })
}) })

View File

@ -11,6 +11,24 @@ async function register ({
getWeight: () => 30, 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) { login (body) {
if (body.id === 'laguna' && body.password === 'laguna password') { if (body.id === 'laguna' && body.password === 'laguna password') {
return Promise.resolve({ return Promise.resolve({

View File

@ -10,14 +10,21 @@ import {
setAccessTokensToServers, setAccessTokensToServers,
uninstallPlugin, uninstallPlugin,
updateMyUser, updateMyUser,
userLogin userLogin,
wait,
login, refreshToken
} from '../../../shared/extra-utils' } from '../../../shared/extra-utils'
import { User, UserRole } from '@shared/models' import { User, UserRole } from '@shared/models'
import { expect } from 'chai' import { expect } from 'chai'
describe('Test id and pass auth plugins', function () { describe('Test id and pass auth plugins', function () {
let server: ServerInfo let server: ServerInfo
let crashToken: string
let crashAccessToken: string
let crashRefreshToken: string
let lagunaAccessToken: string
let lagunaRefreshToken: string
before(async function () { before(async function () {
this.timeout(30000) 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 () { 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 const body: User = res.body
expect(body.username).to.equal('crash') expect(body.username).to.equal('crash')
expect(body.account.displayName).to.equal('Crash Bandicoot') expect(body.account.displayName).to.equal('Crash Bandicoot')
expect(body.role).to.equal(UserRole.MODERATOR) expect(body.role).to.equal(UserRole.MODERATOR)
}
}) })
it('Should login the first Laguna, create the user and use the token', async function () { 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 const body: User = res.body
expect(body.username).to.equal('laguna') expect(body.username).to.equal('laguna')
expect(body.account.displayName).to.equal('laguna') expect(body.account.displayName).to.equal('laguna')
expect(body.role).to.equal(UserRole.USER) 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 () { it('Should update Crash profile', async function () {
await updateMyUser({ await updateMyUser({
url: server.url, url: server.url,
accessToken: crashToken, accessToken: crashAccessToken,
displayName: 'Beautiful Crash', displayName: 'Beautiful Crash',
description: 'Mutant eastern barred bandicoot' description: 'Mutant eastern barred bandicoot'
}) })
const res = await getMyUserInformation(server.url, crashToken) const res = await getMyUserInformation(server.url, crashAccessToken)
const body: User = res.body const body: User = res.body
expect(body.account.displayName).to.equal('Beautiful Crash') 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 () { it('Should logout Crash', async function () {
await logout(server.url, crashToken) await logout(server.url, crashAccessToken)
}) })
it('Should have logged out Crash', async function () { it('Should have logged out Crash', async function () {
await getMyUserInformation(server.url, crashToken, 401)
await waitUntilLog(server, 'On logout for auth 1 - 2') 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 () { 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 const body: User = res.body
expect(body.username).to.equal('crash') 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) 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 () { it('Should uninstall the plugin one and do not login existing Crash', async function () {
await uninstallPlugin({ await uninstallPlugin({
url: server.url, url: server.url,

View File

@ -46,6 +46,8 @@ declare module 'express' {
} }
} }
refreshTokenAuthName?: string
explicitLogout: boolean explicitLogout: boolean
videoAll?: MVideoFullLight videoAll?: MVideoFullLight

View File

@ -43,6 +43,24 @@ async function serverLogin (server: Server) {
return res.body.access_token as string 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) { async function userLogin (server: Server, user: User, expectedStatus = 200) {
const res = await login(server.url, server.client, user, expectedStatus) const res = await login(server.url, server.client, user, expectedStatus)
@ -83,6 +101,7 @@ export {
login, login,
logout, logout,
serverLogin, serverLogin,
refreshToken,
userLogin, userLogin,
getAccessToken, getAccessToken,
setAccessTokensToServers, setAccessTokensToServers,

View File

@ -0,0 +1 @@
export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'

View File

@ -1,4 +1,5 @@
import { UserRole } from '@shared/models' import { UserRole } from '@shared/models'
import { MOAuthToken } from '@server/typings/models'
export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
@ -6,11 +7,16 @@ export interface RegisterServerAuthPassOptions {
// Authentication name (a plugin can register multiple auth strategies) // Authentication name (a plugin can register multiple auth strategies)
authName: string 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 // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
getWeight(): number 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 // Used by PeerTube to login a user
// Returns null if the login failed, or { username, email } on success // Returns null if the login failed, or { username, email } on success
login(body: { login(body: {

View File

@ -1,6 +1,6 @@
import { ContextType } from '@server/helpers/activitypub'
import { SendEmailOptions } from './emailer.model' import { SendEmailOptions } from './emailer.model'
import { VideoResolution } from '@shared/models' import { VideoResolution } from '@shared/models'
import { ContextType } from '../activitypub/context'
export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed'