aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-04-24 11:33:01 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-05-04 16:21:39 +0200
commite307e4fce39853d445d086f92b8c556c363ee15d (patch)
tree0f3faaf3c73222db0fb55b72260c787aeeeb05eb
parente1c5503114deef954731904695cd40dccfcef555 (diff)
downloadPeerTube-e307e4fce39853d445d086f92b8c556c363ee15d.tar.gz
PeerTube-e307e4fce39853d445d086f92b8c556c363ee15d.tar.zst
PeerTube-e307e4fce39853d445d086f92b8c556c363ee15d.zip
Add ability for auth plugins to hook tokens validity
-rw-r--r--server/controllers/api/users/token.ts3
-rw-r--r--server/helpers/activitypub.ts3
-rw-r--r--server/lib/activitypub/send/send-create.ts2
-rw-r--r--server/lib/activitypub/send/utils.ts2
-rw-r--r--server/lib/auth.ts128
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts3
-rw-r--r--server/lib/oauth-model.ts62
-rw-r--r--server/lib/plugins/plugin-manager.ts39
-rw-r--r--server/models/oauth/oauth-token.ts55
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js18
-rw-r--r--server/tests/plugins/id-and-pass-auth.ts85
-rw-r--r--server/typings/express.ts2
-rw-r--r--shared/extra-utils/users/login.ts19
-rw-r--r--shared/models/activitypub/context.ts1
-rw-r--r--shared/models/plugins/register-server-auth.model.ts8
-rw-r--r--shared/models/server/job.model.ts2
16 files changed, 299 insertions, 133 deletions
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 9694f9e5e..f4be228f6 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -20,8 +20,7 @@ tokensRouter.post('/token',
20 20
21tokensRouter.post('/revoke-token', 21tokensRouter.post('/revoke-token',
22 authenticate, 22 authenticate,
23 asyncMiddleware(handleTokenRevocation), 23 asyncMiddleware(handleTokenRevocation)
24 tokenSuccess
25) 24)
26 25
27// --------------------------------------------------------------------------- 26// ---------------------------------------------------------------------------
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 2d49e6869..aeb8fde01 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -7,8 +7,7 @@ import { signJsonLDObject } from './peertube-crypto'
7import { pageToStartAndCount } from './core-utils' 7import { pageToStartAndCount } from './core-utils'
8import { URL } from 'url' 8import { URL } from 'url'
9import { MActor, MVideoAccountLight } from '../typings/models' 9import { MActor, MVideoAccountLight } from '../typings/models'
10 10import { ContextType } from '@shared/models/activitypub/context'
11export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
12 11
13function getContextData (type: ContextType) { 12function getContextData (type: ContextType) {
14 const context: any[] = [ 13 const context: any[] = [
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 0635c7b66..e521cabbc 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -15,8 +15,8 @@ import {
15 MVideoRedundancyFileVideo, 15 MVideoRedundancyFileVideo,
16 MVideoRedundancyStreamingPlaylistVideo 16 MVideoRedundancyStreamingPlaylistVideo
17} from '../../../typings/models' 17} from '../../../typings/models'
18import { ContextType } from '@server/helpers/activitypub'
19import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context'
20 20
21async function sendCreateVideo (video: MVideoAP, t: Transaction) { 21async function sendCreateVideo (video: MVideoAP, t: Transaction) {
22 if (!video.hasPrivacyForFederation()) return undefined 22 if (!video.hasPrivacyForFederation()) return undefined
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts
index 0dfcc51be..44a8926e5 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -7,8 +7,8 @@ import { JobQueue } from '../../job-queue'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
8import { afterCommitIfTransaction } from '../../../helpers/database-utils' 8import { afterCommitIfTransaction } from '../../../helpers/database-utils'
9import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models' 9import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models'
10import { ContextType } from '@server/helpers/activitypub'
11import { getServerActor } from '@server/models/application/application' 10import { getServerActor } from '@server/models/application/application'
11import { ContextType } from '@shared/models/activitypub/context'
12 12
13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
14 byActor: MActorLight 14 byActor: MActorLight
diff --git a/server/lib/auth.ts b/server/lib/auth.ts
index 3495571db..c2a6fcaff 100644
--- a/server/lib/auth.ts
+++ b/server/lib/auth.ts
@@ -6,6 +6,7 @@ import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-s
6import { logger } from '@server/helpers/logger' 6import { logger } from '@server/helpers/logger'
7import { UserRole } from '@shared/models' 7import { UserRole } from '@shared/models'
8import { revokeToken } from '@server/lib/oauth-model' 8import { revokeToken } from '@server/lib/oauth-model'
9import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
9 10
10const oAuthServer = new OAuthServer({ 11const oAuthServer = new OAuthServer({
11 useErrorHandler: true, 12 useErrorHandler: true,
@@ -20,6 +21,74 @@ function onExternalAuthPlugin (npmName: string, username: string, email: string)
20} 21}
21 22
22async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { 23async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
24 const grantType = req.body.grant_type
25
26 if (grantType === 'password') await proxifyPasswordGrant(req, res)
27 else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res)
28
29 return forwardTokenReq(req, res, next)
30}
31
32async function handleTokenRevocation (req: express.Request, res: express.Response) {
33 const token = res.locals.oauth.token
34
35 res.locals.explicitLogout = true
36 await revokeToken(token)
37
38 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
39 // oAuthServer.revoke(req, res, err => {
40 // if (err) {
41 // logger.warn('Error in revoke token handler.', { err })
42 //
43 // return res.status(err.status)
44 // .json({
45 // error: err.message,
46 // code: err.name
47 // })
48 // .end()
49 // }
50 // })
51
52 return res.sendStatus(200)
53}
54
55// ---------------------------------------------------------------------------
56
57export {
58 oAuthServer,
59 handleIdAndPassLogin,
60 onExternalAuthPlugin,
61 handleTokenRevocation
62}
63
64// ---------------------------------------------------------------------------
65
66function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) {
67 return oAuthServer.token()(req, res, err => {
68 if (err) {
69 logger.warn('Login error.', { err })
70
71 return res.status(err.status)
72 .json({
73 error: err.message,
74 code: err.name
75 })
76 .end()
77 }
78
79 return next()
80 })
81}
82
83async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
84 const refreshToken = req.body.refresh_token
85 if (!refreshToken) return
86
87 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
88 if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName
89}
90
91async function proxifyPasswordGrant (req: express.Request, res: express.Response) {
23 const plugins = PluginManager.Instance.getIdAndPassAuths() 92 const plugins = PluginManager.Instance.getIdAndPassAuths()
24 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] 93 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
25 94
@@ -76,64 +145,7 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response
76 } 145 }
77 } 146 }
78 147
79 break 148 return
80 } 149 }
81 } 150 }
82
83 return localLogin(req, res, next)
84}
85
86async function handleTokenRevocation (req: express.Request, res: express.Response) {
87 const token = res.locals.oauth.token
88
89 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName)
90
91 await revokeToken(token)
92 .catch(err => {
93 logger.error('Cannot revoke token.', err)
94 })
95
96 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
97 // oAuthServer.revoke(req, res, err => {
98 // if (err) {
99 // logger.warn('Error in revoke token handler.', { err })
100 //
101 // return res.status(err.status)
102 // .json({
103 // error: err.message,
104 // code: err.name
105 // })
106 // .end()
107 // }
108 // })
109
110 return res.sendStatus(200)
111}
112
113// ---------------------------------------------------------------------------
114
115export {
116 oAuthServer,
117 handleIdAndPassLogin,
118 onExternalAuthPlugin,
119 handleTokenRevocation
120}
121
122// ---------------------------------------------------------------------------
123
124function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
125 return oAuthServer.token()(req, res, err => {
126 if (err) {
127 logger.warn('Login error.', { err })
128
129 return res.status(err.status)
130 .json({
131 error: err.message,
132 code: err.name
133 })
134 .end()
135 }
136
137 return next()
138 })
139} 151}
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index 437ea06fc..bcb49a731 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -1,9 +1,10 @@
1import { buildSignedActivity, ContextType } from '../../../../helpers/activitypub' 1import { buildSignedActivity } from '../../../../helpers/activitypub'
2import { ActorModel } from '../../../../models/activitypub/actor' 2import { ActorModel } from '../../../../models/activitypub/actor'
3import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' 3import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants'
4import { MActor } from '../../../../typings/models' 4import { MActor } from '../../../../typings/models'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { buildDigest } from '@server/helpers/peertube-crypto' 6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context'
7 8
8type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } 9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number }
9 10
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 7a6ed63be..6eb0e4473 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -1,4 +1,3 @@
1import * as Bluebird from 'bluebird'
2import * as express from 'express' 1import * as express from 'express'
3import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
4import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
@@ -47,22 +46,33 @@ function clearCacheByToken (token: string) {
47 } 46 }
48} 47}
49 48
50function getAccessToken (bearerToken: string) { 49async function getAccessToken (bearerToken: string) {
51 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') 50 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
52 51
53 if (!bearerToken) return Bluebird.resolve(undefined) 52 if (!bearerToken) return undefined
54 53
55 if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken)) 54 let tokenModel: MOAuthTokenUser
56 55
57 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 56 if (accessTokenCache.has(bearerToken)) {
58 .then(tokenModel => { 57 tokenModel = accessTokenCache.get(bearerToken)
59 if (tokenModel) { 58 } else {
60 accessTokenCache.set(bearerToken, tokenModel) 59 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
61 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
62 }
63 60
64 return tokenModel 61 if (tokenModel) {
65 }) 62 accessTokenCache.set(bearerToken, tokenModel)
63 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
64 }
65 }
66
67 if (!tokenModel) return undefined
68
69 if (tokenModel.User.pluginAuth) {
70 const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access')
71
72 if (valid !== true) return undefined
73 }
74
75 return tokenModel
66} 76}
67 77
68function getClient (clientId: string, clientSecret: string) { 78function getClient (clientId: string, clientSecret: string) {
@@ -71,14 +81,27 @@ function getClient (clientId: string, clientSecret: string) {
71 return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) 81 return OAuthClientModel.getByIdAndSecret(clientId, clientSecret)
72} 82}
73 83
74function getRefreshToken (refreshToken: string) { 84async function getRefreshToken (refreshToken: string) {
75 logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') 85 logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').')
76 86
77 return OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) 87 const tokenInfo = await OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken)
88 if (!tokenInfo) return undefined
89
90 const tokenModel = tokenInfo.token
91
92 if (tokenModel.User.pluginAuth) {
93 const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh')
94
95 if (valid !== true) return undefined
96 }
97
98 return tokenInfo
78} 99}
79 100
80async function getUser (usernameOrEmail: string, password: string) { 101async function getUser (usernameOrEmail: string, password: string) {
81 const res: express.Response = this.request.res 102 const res: express.Response = this.request.res
103
104 // Special treatment coming from a plugin
82 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { 105 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) {
83 const obj = res.locals.bypassLogin 106 const obj = res.locals.bypassLogin
84 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) 107 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
@@ -110,7 +133,7 @@ async function getUser (usernameOrEmail: string, password: string) {
110 return user 133 return user
111} 134}
112 135
113async function revokeToken (tokenInfo: TokenInfo) { 136async function revokeToken (tokenInfo: { refreshToken: string }) {
114 const res: express.Response = this.request.res 137 const res: express.Response = this.request.res
115 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 138 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
116 139
@@ -133,9 +156,12 @@ async function revokeToken (tokenInfo: TokenInfo) {
133async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 156async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
134 const res: express.Response = this.request.res 157 const res: express.Response = this.request.res
135 158
136 const authName = res.locals.bypassLogin?.bypass === true 159 let authName: string = null
137 ? res.locals.bypassLogin.authName 160 if (res.locals.bypassLogin?.bypass === true) {
138 : null 161 authName = res.locals.bypassLogin.authName
162 } else if (res.locals.refreshTokenAuthName) {
163 authName = res.locals.refreshTokenAuthName
164 }
139 165
140 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 166 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
141 167
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 9d646b689..c64ca60aa 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -21,6 +21,7 @@ import { ClientHtml } from '../client-html'
21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' 21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
22import { RegisterHelpersStore } from './register-helpers-store' 22import { RegisterHelpersStore } from './register-helpers-store'
23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' 23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
24import { MOAuthTokenUser } from '@server/typings/models'
24 25
25export interface RegisteredPlugin { 26export interface RegisteredPlugin {
26 npmName: string 27 npmName: string
@@ -133,13 +134,11 @@ export class PluginManager implements ServerHook {
133 } 134 }
134 135
135 onLogout (npmName: string, authName: string) { 136 onLogout (npmName: string, authName: string) {
136 const plugin = this.getRegisteredPluginOrTheme(npmName) 137 const auth = this.getAuth(npmName, authName)
137 if (!plugin || plugin.type !== PluginType.PLUGIN) return
138 138
139 const auth = plugin.registerHelpersStore.getIdAndPassAuths() 139 if (auth?.onLogout) {
140 .find(a => a.authName === authName) 140 logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
141 141
142 if (auth.onLogout) {
143 try { 142 try {
144 auth.onLogout() 143 auth.onLogout()
145 } catch (err) { 144 } catch (err) {
@@ -148,6 +147,28 @@ export class PluginManager implements ServerHook {
148 } 147 }
149 } 148 }
150 149
150 async isTokenValid (token: MOAuthTokenUser, type: 'access' | 'refresh') {
151 const auth = this.getAuth(token.User.pluginAuth, token.authName)
152 if (!auth) return true
153
154 if (auth.hookTokenValidity) {
155 try {
156 const { valid } = await auth.hookTokenValidity({ token, type })
157
158 if (valid === false) {
159 logger.info('Rejecting %s token validity from auth %s of plugin %s', type, token.authName, token.User.pluginAuth)
160 }
161
162 return valid
163 } catch (err) {
164 logger.warn('Cannot run check token validity from auth %s of plugin %s.', token.authName, token.User.pluginAuth, { err })
165 return true
166 }
167 }
168
169 return true
170 }
171
151 // ###################### Hooks ###################### 172 // ###################### Hooks ######################
152 173
153 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 174 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
@@ -453,6 +474,14 @@ export class PluginManager implements ServerHook {
453 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) 474 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName)
454 } 475 }
455 476
477 private getAuth (npmName: string, authName: string) {
478 const plugin = this.getRegisteredPluginOrTheme(npmName)
479 if (!plugin || plugin.type !== PluginType.PLUGIN) return null
480
481 return plugin.registerHelpersStore.getIdAndPassAuths()
482 .find(a => a.authName === authName)
483 }
484
456 // ###################### Private getters ###################### 485 // ###################### Private getters ######################
457 486
458 private getRegisteredPluginsOrThemes (type: PluginType) { 487 private getRegisteredPluginsOrThemes (type: PluginType) {
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index e73c4be7d..3541b6103 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -30,6 +30,7 @@ export type OAuthTokenInfo = {
30 user: { 30 user: {
31 id: number 31 id: number
32 } 32 }
33 token: MOAuthTokenUser
33} 34}
34 35
35enum ScopeNames { 36enum ScopeNames {
@@ -136,33 +137,43 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
136 return clearCacheByToken(token.accessToken) 137 return clearCacheByToken(token.accessToken)
137 } 138 }
138 139
140 static loadByRefreshToken (refreshToken: string) {
141 const query = {
142 where: { refreshToken }
143 }
144
145 return OAuthTokenModel.findOne(query)
146 }
147
139 static getByRefreshTokenAndPopulateClient (refreshToken: string) { 148 static getByRefreshTokenAndPopulateClient (refreshToken: string) {
140 const query = { 149 const query = {
141 where: { 150 where: {
142 refreshToken: refreshToken 151 refreshToken
143 }, 152 },
144 include: [ OAuthClientModel ] 153 include: [ OAuthClientModel ]
145 } 154 }
146 155
147 return OAuthTokenModel.findOne(query) 156 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
148 .then(token => { 157 .findOne(query)
149 if (!token) return null 158 .then(token => {
150 159 if (!token) return null
151 return { 160
152 refreshToken: token.refreshToken, 161 return {
153 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 162 refreshToken: token.refreshToken,
154 client: { 163 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
155 id: token.oAuthClientId 164 client: {
156 }, 165 id: token.oAuthClientId
157 user: { 166 },
158 id: token.userId 167 user: {
159 } 168 id: token.userId
160 } as OAuthTokenInfo 169 },
161 }) 170 token
162 .catch(err => { 171 } as OAuthTokenInfo
163 logger.error('getRefreshToken error.', { err }) 172 })
164 throw err 173 .catch(err => {
165 }) 174 logger.error('getRefreshToken error.', { err })
175 throw err
176 })
166 } 177 }
167 178
168 static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> { 179 static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> {
@@ -184,14 +195,14 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
184 static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> { 195 static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> {
185 const query = { 196 const query = {
186 where: { 197 where: {
187 refreshToken: refreshToken 198 refreshToken
188 } 199 }
189 } 200 }
190 201
191 return OAuthTokenModel.scope(ScopeNames.WITH_USER) 202 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
192 .findOne(query) 203 .findOne(query)
193 .then(token => { 204 .then(token => {
194 if (!token) return new OAuthTokenModel() 205 if (!token) return undefined
195 206
196 return Object.assign(token, { user: token.User }) 207 return Object.assign(token, { user: token.User })
197 }) 208 })
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
index c0e560019..ceab7b60d 100644
--- a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
@@ -11,6 +11,24 @@ async function register ({
11 11
12 getWeight: () => 30, 12 getWeight: () => 30,
13 13
14 hookTokenValidity: (options) => {
15 if (options.type === 'refresh') {
16 return { valid: false }
17 }
18
19 if (options.type === 'access') {
20 const token = options.token
21 const now = new Date()
22 now.setTime(now.getTime() - 5000)
23
24 const createdAt = new Date(token.createdAt)
25
26 return { valid: createdAt.getTime() >= now.getTime() }
27 }
28
29 return { valid: true }
30 },
31
14 login (body) { 32 login (body) {
15 if (body.id === 'laguna' && body.password === 'laguna password') { 33 if (body.id === 'laguna' && body.password === 'laguna password') {
16 return Promise.resolve({ 34 return Promise.resolve({
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts
index 45fa7856c..0268d35a0 100644
--- a/server/tests/plugins/id-and-pass-auth.ts
+++ b/server/tests/plugins/id-and-pass-auth.ts
@@ -10,14 +10,21 @@ import {
10 setAccessTokensToServers, 10 setAccessTokensToServers,
11 uninstallPlugin, 11 uninstallPlugin,
12 updateMyUser, 12 updateMyUser,
13 userLogin 13 userLogin,
14 wait,
15 login, refreshToken
14} from '../../../shared/extra-utils' 16} from '../../../shared/extra-utils'
15import { User, UserRole } from '@shared/models' 17import { User, UserRole } from '@shared/models'
16import { expect } from 'chai' 18import { expect } from 'chai'
17 19
18describe('Test id and pass auth plugins', function () { 20describe('Test id and pass auth plugins', function () {
19 let server: ServerInfo 21 let server: ServerInfo
20 let crashToken: string 22
23 let crashAccessToken: string
24 let crashRefreshToken: string
25
26 let lagunaAccessToken: string
27 let lagunaRefreshToken: string
21 28
22 before(async function () { 29 before(async function () {
23 this.timeout(30000) 30 this.timeout(30000)
@@ -50,36 +57,64 @@ describe('Test id and pass auth plugins', function () {
50 }) 57 })
51 58
52 it('Should login Crash, create the user and use the token', async function () { 59 it('Should login Crash, create the user and use the token', async function () {
53 crashToken = await userLogin(server, { username: 'crash', password: 'crash password' }) 60 {
61 const res = await login(server.url, server.client, { username: 'crash', password: 'crash password' })
62 crashAccessToken = res.body.access_token
63 crashRefreshToken = res.body.refresh_token
64 }
54 65
55 const res = await getMyUserInformation(server.url, crashToken) 66 {
67 const res = await getMyUserInformation(server.url, crashAccessToken)
56 68
57 const body: User = res.body 69 const body: User = res.body
58 expect(body.username).to.equal('crash') 70 expect(body.username).to.equal('crash')
59 expect(body.account.displayName).to.equal('Crash Bandicoot') 71 expect(body.account.displayName).to.equal('Crash Bandicoot')
60 expect(body.role).to.equal(UserRole.MODERATOR) 72 expect(body.role).to.equal(UserRole.MODERATOR)
73 }
61 }) 74 })
62 75
63 it('Should login the first Laguna, create the user and use the token', async function () { 76 it('Should login the first Laguna, create the user and use the token', async function () {
64 const accessToken = await userLogin(server, { username: 'laguna', password: 'laguna password' }) 77 {
78 const res = await login(server.url, server.client, { username: 'laguna', password: 'laguna password' })
79 lagunaAccessToken = res.body.access_token
80 lagunaRefreshToken = res.body.refresh_token
81 }
65 82
66 const res = await getMyUserInformation(server.url, accessToken) 83 {
84 const res = await getMyUserInformation(server.url, lagunaAccessToken)
67 85
68 const body: User = res.body 86 const body: User = res.body
69 expect(body.username).to.equal('laguna') 87 expect(body.username).to.equal('laguna')
70 expect(body.account.displayName).to.equal('laguna') 88 expect(body.account.displayName).to.equal('laguna')
71 expect(body.role).to.equal(UserRole.USER) 89 expect(body.role).to.equal(UserRole.USER)
90 }
91 })
92
93 it('Should refresh crash token, but not laguna token', async function () {
94 {
95 const resRefresh = await refreshToken(server, crashRefreshToken)
96 crashAccessToken = resRefresh.body.access_token
97 crashRefreshToken = resRefresh.body.refresh_token
98
99 const res = await getMyUserInformation(server.url, crashAccessToken)
100 const user: User = res.body
101 expect(user.username).to.equal('crash')
102 }
103
104 {
105 await refreshToken(server, lagunaRefreshToken, 400)
106 }
72 }) 107 })
73 108
74 it('Should update Crash profile', async function () { 109 it('Should update Crash profile', async function () {
75 await updateMyUser({ 110 await updateMyUser({
76 url: server.url, 111 url: server.url,
77 accessToken: crashToken, 112 accessToken: crashAccessToken,
78 displayName: 'Beautiful Crash', 113 displayName: 'Beautiful Crash',
79 description: 'Mutant eastern barred bandicoot' 114 description: 'Mutant eastern barred bandicoot'
80 }) 115 })
81 116
82 const res = await getMyUserInformation(server.url, crashToken) 117 const res = await getMyUserInformation(server.url, crashAccessToken)
83 118
84 const body: User = res.body 119 const body: User = res.body
85 expect(body.account.displayName).to.equal('Beautiful Crash') 120 expect(body.account.displayName).to.equal('Beautiful Crash')
@@ -87,19 +122,19 @@ describe('Test id and pass auth plugins', function () {
87 }) 122 })
88 123
89 it('Should logout Crash', async function () { 124 it('Should logout Crash', async function () {
90 await logout(server.url, crashToken) 125 await logout(server.url, crashAccessToken)
91 }) 126 })
92 127
93 it('Should have logged out Crash', async function () { 128 it('Should have logged out Crash', async function () {
94 await getMyUserInformation(server.url, crashToken, 401)
95
96 await waitUntilLog(server, 'On logout for auth 1 - 2') 129 await waitUntilLog(server, 'On logout for auth 1 - 2')
130
131 await getMyUserInformation(server.url, crashAccessToken, 401)
97 }) 132 })
98 133
99 it('Should login Crash and keep the old existing profile', async function () { 134 it('Should login Crash and keep the old existing profile', async function () {
100 crashToken = await userLogin(server, { username: 'crash', password: 'crash password' }) 135 crashAccessToken = await userLogin(server, { username: 'crash', password: 'crash password' })
101 136
102 const res = await getMyUserInformation(server.url, crashToken) 137 const res = await getMyUserInformation(server.url, crashAccessToken)
103 138
104 const body: User = res.body 139 const body: User = res.body
105 expect(body.username).to.equal('crash') 140 expect(body.username).to.equal('crash')
@@ -108,6 +143,14 @@ describe('Test id and pass auth plugins', function () {
108 expect(body.role).to.equal(UserRole.MODERATOR) 143 expect(body.role).to.equal(UserRole.MODERATOR)
109 }) 144 })
110 145
146 it('Should correctly auth token of laguna', async function () {
147 this.timeout(10000)
148
149 await wait(5000)
150
151 await getMyUserInformation(server.url, lagunaAccessToken, 401)
152 })
153
111 it('Should uninstall the plugin one and do not login existing Crash', async function () { 154 it('Should uninstall the plugin one and do not login existing Crash', async function () {
112 await uninstallPlugin({ 155 await uninstallPlugin({
113 url: server.url, 156 url: server.url,
diff --git a/server/typings/express.ts b/server/typings/express.ts
index 2d12a486a..e6e120403 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -46,6 +46,8 @@ declare module 'express' {
46 } 46 }
47 } 47 }
48 48
49 refreshTokenAuthName?: string
50
49 explicitLogout: boolean 51 explicitLogout: boolean
50 52
51 videoAll?: MVideoFullLight 53 videoAll?: MVideoFullLight
diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts
index 2d68337a6..b12b51b8c 100644
--- a/shared/extra-utils/users/login.ts
+++ b/shared/extra-utils/users/login.ts
@@ -43,6 +43,24 @@ async function serverLogin (server: Server) {
43 return res.body.access_token as string 43 return res.body.access_token as string
44} 44}
45 45
46function refreshToken (server: ServerInfo, refreshToken: string, expectedStatus = 200) {
47 const path = '/api/v1/users/token'
48
49 const body = {
50 client_id: server.client.id,
51 client_secret: server.client.secret,
52 refresh_token: refreshToken,
53 response_type: 'code',
54 grant_type: 'refresh_token'
55 }
56
57 return request(server.url)
58 .post(path)
59 .type('form')
60 .send(body)
61 .expect(expectedStatus)
62}
63
46async function userLogin (server: Server, user: User, expectedStatus = 200) { 64async function userLogin (server: Server, user: User, expectedStatus = 200) {
47 const res = await login(server.url, server.client, user, expectedStatus) 65 const res = await login(server.url, server.client, user, expectedStatus)
48 66
@@ -83,6 +101,7 @@ export {
83 login, 101 login,
84 logout, 102 logout,
85 serverLogin, 103 serverLogin,
104 refreshToken,
86 userLogin, 105 userLogin,
87 getAccessToken, 106 getAccessToken,
88 setAccessTokensToServers, 107 setAccessTokensToServers,
diff --git a/shared/models/activitypub/context.ts b/shared/models/activitypub/context.ts
new file mode 100644
index 000000000..bd795a2fd
--- /dev/null
+++ b/shared/models/activitypub/context.ts
@@ -0,0 +1 @@
export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
diff --git a/shared/models/plugins/register-server-auth.model.ts b/shared/models/plugins/register-server-auth.model.ts
index dc46dcbc8..403a49994 100644
--- a/shared/models/plugins/register-server-auth.model.ts
+++ b/shared/models/plugins/register-server-auth.model.ts
@@ -1,4 +1,5 @@
1import { UserRole } from '@shared/models' 1import { UserRole } from '@shared/models'
2import { MOAuthToken } from '@server/typings/models'
2 3
3export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions 4export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
4 5
@@ -6,11 +7,16 @@ export interface RegisterServerAuthPassOptions {
6 // Authentication name (a plugin can register multiple auth strategies) 7 // Authentication name (a plugin can register multiple auth strategies)
7 authName: string 8 authName: string
8 9
9 onLogout?: Function 10 // Called by PeerTube when a user from your plugin logged out
11 onLogout?(): void
10 12
11 // Weight of this authentication so PeerTube tries the auth methods in DESC weight order 13 // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
12 getWeight(): number 14 getWeight(): number
13 15
16 // Your plugin can hook PeerTube access/refresh token validity
17 // So you can control for your plugin the user session lifetime
18 hookTokenValidity?(options: { token: MOAuthToken, type: 'access' | 'refresh' }): Promise<{ valid: boolean }>
19
14 // Used by PeerTube to login a user 20 // Used by PeerTube to login a user
15 // Returns null if the login failed, or { username, email } on success 21 // Returns null if the login failed, or { username, email } on success
16 login(body: { 22 login(body: {
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index 694361276..57d61c480 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -1,6 +1,6 @@
1import { ContextType } from '@server/helpers/activitypub'
2import { SendEmailOptions } from './emailer.model' 1import { SendEmailOptions } from './emailer.model'
3import { VideoResolution } from '@shared/models' 2import { VideoResolution } from '@shared/models'
3import { ContextType } from '../activitypub/context'
4 4
5export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' 5export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed'
6 6