Add ability for auth plugins to hook tokens validity
This commit is contained in:
parent
e1c5503114
commit
e307e4fce3
|
@ -20,8 +20,7 @@ tokensRouter.post('/token',
|
||||||
|
|
||||||
tokensRouter.post('/revoke-token',
|
tokensRouter.post('/revoke-token',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(handleTokenRevocation),
|
asyncMiddleware(handleTokenRevocation)
|
||||||
tokenSuccess
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -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[] = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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 + '.')
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 })
|
||||||
})
|
})
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -46,6 +46,8 @@ declare module 'express' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshTokenAuthName?: string
|
||||||
|
|
||||||
explicitLogout: boolean
|
explicitLogout: boolean
|
||||||
|
|
||||||
videoAll?: MVideoFullLight
|
videoAll?: MVideoFullLight
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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 { 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: {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user