diff options
author | Chocobozzz <me@florianbigard.com> | 2020-04-24 11:33:01 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-05-04 16:21:39 +0200 |
commit | e307e4fce39853d445d086f92b8c556c363ee15d (patch) | |
tree | 0f3faaf3c73222db0fb55b72260c787aeeeb05eb /server/lib | |
parent | e1c5503114deef954731904695cd40dccfcef555 (diff) | |
download | PeerTube-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.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/send/utils.ts | 2 | ||||
-rw-r--r-- | server/lib/auth.ts | 128 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | 3 | ||||
-rw-r--r-- | server/lib/oauth-model.ts | 62 | ||||
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 39 |
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' |
18 | import { ContextType } from '@server/helpers/activitypub' | ||
19 | import { getServerActor } from '@server/models/application/application' | 18 | import { getServerActor } from '@server/models/application/application' |
19 | import { ContextType } from '@shared/models/activitypub/context' | ||
20 | 20 | ||
21 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { | 21 | async 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' | |||
7 | import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' | 7 | import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' |
8 | import { afterCommitIfTransaction } from '../../../helpers/database-utils' | 8 | import { afterCommitIfTransaction } from '../../../helpers/database-utils' |
9 | import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models' | 9 | import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models' |
10 | import { ContextType } from '@server/helpers/activitypub' | ||
11 | import { getServerActor } from '@server/models/application/application' | 10 | import { getServerActor } from '@server/models/application/application' |
11 | import { ContextType } from '@shared/models/activitypub/context' | ||
12 | 12 | ||
13 | async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { | 13 | async 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 | |||
6 | import { logger } from '@server/helpers/logger' | 6 | import { logger } from '@server/helpers/logger' |
7 | import { UserRole } from '@shared/models' | 7 | import { UserRole } from '@shared/models' |
8 | import { revokeToken } from '@server/lib/oauth-model' | 8 | import { revokeToken } from '@server/lib/oauth-model' |
9 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | ||
9 | 10 | ||
10 | const oAuthServer = new OAuthServer({ | 11 | const 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 | ||
22 | async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | 23 | async 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 | |||
32 | async 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 | |||
57 | export { | ||
58 | oAuthServer, | ||
59 | handleIdAndPassLogin, | ||
60 | onExternalAuthPlugin, | ||
61 | handleTokenRevocation | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | function 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 | |||
83 | async 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 | |||
91 | async 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 | |||
86 | async 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 | |||
115 | export { | ||
116 | oAuthServer, | ||
117 | handleIdAndPassLogin, | ||
118 | onExternalAuthPlugin, | ||
119 | handleTokenRevocation | ||
120 | } | ||
121 | |||
122 | // --------------------------------------------------------------------------- | ||
123 | |||
124 | function 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 @@ | |||
1 | import { buildSignedActivity, ContextType } from '../../../../helpers/activitypub' | 1 | import { buildSignedActivity } from '../../../../helpers/activitypub' |
2 | import { ActorModel } from '../../../../models/activitypub/actor' | 2 | import { ActorModel } from '../../../../models/activitypub/actor' |
3 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' | 3 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' |
4 | import { MActor } from '../../../../typings/models' | 4 | import { MActor } from '../../../../typings/models' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { buildDigest } from '@server/helpers/peertube-crypto' | 6 | import { buildDigest } from '@server/helpers/peertube-crypto' |
7 | import { ContextType } from '@shared/models/activitypub/context' | ||
7 | 8 | ||
8 | type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } | 9 | type 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as express from 'express' | 1 | import * as express from 'express' |
3 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
4 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
@@ -47,22 +46,33 @@ function clearCacheByToken (token: string) { | |||
47 | } | 46 | } |
48 | } | 47 | } |
49 | 48 | ||
50 | function getAccessToken (bearerToken: string) { | 49 | async 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 | ||
68 | function getClient (clientId: string, clientSecret: string) { | 78 | function 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 | ||
74 | function getRefreshToken (refreshToken: string) { | 84 | async 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 | ||
80 | async function getUser (usernameOrEmail: string, password: string) { | 101 | async 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 | ||
113 | async function revokeToken (tokenInfo: TokenInfo) { | 136 | async 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) { | |||
133 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 156 | async 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' | |||
21 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' | 21 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' |
22 | import { RegisterHelpersStore } from './register-helpers-store' | 22 | import { RegisterHelpersStore } from './register-helpers-store' |
23 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' | 23 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' |
24 | import { MOAuthTokenUser } from '@server/typings/models' | ||
24 | 25 | ||
25 | export interface RegisteredPlugin { | 26 | export 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) { |