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 | |
parent | e1c5503114deef954731904695cd40dccfcef555 (diff) | |
download | PeerTube-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.ts | 3 | ||||
-rw-r--r-- | server/helpers/activitypub.ts | 3 | ||||
-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 | ||||
-rw-r--r-- | server/models/oauth/oauth-token.ts | 55 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js | 18 | ||||
-rw-r--r-- | server/tests/plugins/id-and-pass-auth.ts | 85 | ||||
-rw-r--r-- | server/typings/express.ts | 2 | ||||
-rw-r--r-- | shared/extra-utils/users/login.ts | 19 | ||||
-rw-r--r-- | shared/models/activitypub/context.ts | 1 | ||||
-rw-r--r-- | shared/models/plugins/register-server-auth.model.ts | 8 | ||||
-rw-r--r-- | shared/models/server/job.model.ts | 2 |
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 | ||
21 | tokensRouter.post('/revoke-token', | 21 | tokensRouter.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' | |||
7 | import { pageToStartAndCount } from './core-utils' | 7 | import { pageToStartAndCount } from './core-utils' |
8 | import { URL } from 'url' | 8 | import { URL } from 'url' |
9 | import { MActor, MVideoAccountLight } from '../typings/models' | 9 | import { MActor, MVideoAccountLight } from '../typings/models' |
10 | 10 | import { ContextType } from '@shared/models/activitypub/context' | |
11 | export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile' | ||
12 | 11 | ||
13 | function getContextData (type: ContextType) { | 12 | function 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' |
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) { |
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 | ||
35 | enum ScopeNames { | 36 | enum 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' |
15 | import { User, UserRole } from '@shared/models' | 17 | import { User, UserRole } from '@shared/models' |
16 | import { expect } from 'chai' | 18 | import { expect } from 'chai' |
17 | 19 | ||
18 | describe('Test id and pass auth plugins', function () { | 20 | describe('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 | ||
46 | function 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 | |||
46 | async function userLogin (server: Server, user: User, expectedStatus = 200) { | 64 | async 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 @@ | |||
1 | import { UserRole } from '@shared/models' | 1 | import { UserRole } from '@shared/models' |
2 | import { MOAuthToken } from '@server/typings/models' | ||
2 | 3 | ||
3 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions | 4 | export 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 @@ | |||
1 | import { ContextType } from '@server/helpers/activitypub' | ||
2 | import { SendEmailOptions } from './emailer.model' | 1 | import { SendEmailOptions } from './emailer.model' |
3 | import { VideoResolution } from '@shared/models' | 2 | import { VideoResolution } from '@shared/models' |
3 | import { ContextType } from '../activitypub/context' | ||
4 | 4 | ||
5 | export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 5 | export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' |
6 | 6 | ||