aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
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 /server/lib
parente1c5503114deef954731904695cd40dccfcef555 (diff)
downloadPeerTube-e307e4fce39853d445d086f92b8c556c363ee15d.tar.gz
PeerTube-e307e4fce39853d445d086f92b8c556c363ee15d.tar.zst
PeerTube-e307e4fce39853d445d086f92b8c556c363ee15d.zip
Add ability for auth plugins to hook tokens validity
Diffstat (limited to 'server/lib')
-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
6 files changed, 152 insertions, 84 deletions
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) {