tokensRouter.post('/revoke-token',
authenticate,
- asyncMiddleware(handleTokenRevocation),
- tokenSuccess
+ asyncMiddleware(handleTokenRevocation)
)
// ---------------------------------------------------------------------------
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[] = [
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
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
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,
}
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 }[] = []
}
}
- 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()
- })
}
-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 }
-import * as Bluebird from 'bluebird'
import * as express from 'express'
import { AccessDeniedError } from 'oauth2-server'
import { logger } from '../helpers/logger'
}
}
-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) {
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)
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)
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 + '.')
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
}
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) {
}
}
+ 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> {
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) {
user: {
id: number
}
+ token: MOAuthTokenUser
}
enum ScopeNames {
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> {
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 })
})
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({
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)
})
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')
})
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')
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,
}
}
+ refreshTokenAuthName?: string
+
explicitLogout: boolean
videoAll?: MVideoFullLight
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)
login,
logout,
serverLogin,
+ refreshToken,
userLogin,
getAccessToken,
setAccessTokensToServers,
--- /dev/null
+export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
import { UserRole } from '@shared/models'
+import { MOAuthToken } from '@server/typings/models'
export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
// 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: {
-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'