diff options
author | Chocobozzz <me@florianbigard.com> | 2020-04-23 11:36:50 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-05-04 16:21:39 +0200 |
commit | e1c5503114deef954731904695cd40dccfcef555 (patch) | |
tree | 72cec4ee691a3362a7d024dc830d215a6b2c800a /server/lib | |
parent | 8dc8a34ee8428e7657414115d1c137592efa174d (diff) | |
download | PeerTube-e1c5503114deef954731904695cd40dccfcef555.tar.gz PeerTube-e1c5503114deef954731904695cd40dccfcef555.tar.zst PeerTube-e1c5503114deef954731904695cd40dccfcef555.zip |
Support logout and add id and pass tests
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/auth.ts | 50 | ||||
-rw-r--r-- | server/lib/job-queue/job-queue.ts | 23 | ||||
-rw-r--r-- | server/lib/oauth-model.ts | 19 | ||||
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 20 | ||||
-rw-r--r-- | server/lib/plugins/register-helpers-store.ts | 5 |
5 files changed, 99 insertions, 18 deletions
diff --git a/server/lib/auth.ts b/server/lib/auth.ts index 18d52fa5a..3495571db 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts | |||
@@ -5,6 +5,7 @@ import { PluginManager } from '@server/lib/plugins/plugin-manager' | |||
5 | import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' | 5 | import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' |
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 | 9 | ||
9 | const oAuthServer = new OAuthServer({ | 10 | const oAuthServer = new OAuthServer({ |
10 | useErrorHandler: true, | 11 | useErrorHandler: true, |
@@ -37,8 +38,9 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response | |||
37 | const aWeight = a.registerAuthOptions.getWeight() | 38 | const aWeight = a.registerAuthOptions.getWeight() |
38 | const bWeight = b.registerAuthOptions.getWeight() | 39 | const bWeight = b.registerAuthOptions.getWeight() |
39 | 40 | ||
41 | // DESC weight order | ||
40 | if (aWeight === bWeight) return 0 | 42 | if (aWeight === bWeight) return 0 |
41 | if (aWeight > bWeight) return 1 | 43 | if (aWeight < bWeight) return 1 |
42 | return -1 | 44 | return -1 |
43 | }) | 45 | }) |
44 | 46 | ||
@@ -48,18 +50,24 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response | |||
48 | } | 50 | } |
49 | 51 | ||
50 | for (const pluginAuth of pluginAuths) { | 52 | for (const pluginAuth of pluginAuths) { |
53 | const authOptions = pluginAuth.registerAuthOptions | ||
54 | |||
51 | logger.debug( | 55 | logger.debug( |
52 | 'Using auth method of %s to login %s with weight %d.', | 56 | 'Using auth method %s of plugin %s to login %s with weight %d.', |
53 | pluginAuth.npmName, loginOptions.id, pluginAuth.registerAuthOptions.getWeight() | 57 | authOptions.authName, pluginAuth.npmName, loginOptions.id, authOptions.getWeight() |
54 | ) | 58 | ) |
55 | 59 | ||
56 | const loginResult = await pluginAuth.registerAuthOptions.login(loginOptions) | 60 | const loginResult = await authOptions.login(loginOptions) |
57 | if (loginResult) { | 61 | if (loginResult) { |
58 | logger.info('Login success with plugin %s for %s.', pluginAuth.npmName, loginOptions.id) | 62 | logger.info( |
63 | 'Login success with auth method %s of plugin %s for %s.', | ||
64 | authOptions.authName, pluginAuth.npmName, loginOptions.id | ||
65 | ) | ||
59 | 66 | ||
60 | res.locals.bypassLogin = { | 67 | res.locals.bypassLogin = { |
61 | bypass: true, | 68 | bypass: true, |
62 | pluginName: pluginAuth.npmName, | 69 | pluginName: pluginAuth.npmName, |
70 | authName: authOptions.authName, | ||
63 | user: { | 71 | user: { |
64 | username: loginResult.username, | 72 | username: loginResult.username, |
65 | email: loginResult.email, | 73 | email: loginResult.email, |
@@ -75,12 +83,40 @@ async function handleIdAndPassLogin (req: express.Request, res: express.Response | |||
75 | return localLogin(req, res, next) | 83 | return localLogin(req, res, next) |
76 | } | 84 | } |
77 | 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 | |||
78 | // --------------------------------------------------------------------------- | 113 | // --------------------------------------------------------------------------- |
79 | 114 | ||
80 | export { | 115 | export { |
81 | oAuthServer, | 116 | oAuthServer, |
82 | handleIdAndPassLogin, | 117 | handleIdAndPassLogin, |
83 | onExternalAuthPlugin | 118 | onExternalAuthPlugin, |
119 | handleTokenRevocation | ||
84 | } | 120 | } |
85 | 121 | ||
86 | // --------------------------------------------------------------------------- | 122 | // --------------------------------------------------------------------------- |
@@ -88,6 +124,8 @@ export { | |||
88 | function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | 124 | function localLogin (req: express.Request, res: express.Response, next: express.NextFunction) { |
89 | return oAuthServer.token()(req, res, err => { | 125 | return oAuthServer.token()(req, res, err => { |
90 | if (err) { | 126 | if (err) { |
127 | logger.warn('Login error.', { err }) | ||
128 | |||
91 | return res.status(err.status) | 129 | return res.status(err.status) |
92 | .json({ | 130 | .json({ |
93 | error: err.message, | 131 | error: err.message, |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index d8d64caaf..14e181835 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -2,9 +2,16 @@ import * as Bull from 'bull' | |||
2 | import { | 2 | import { |
3 | ActivitypubFollowPayload, | 3 | ActivitypubFollowPayload, |
4 | ActivitypubHttpBroadcastPayload, | 4 | ActivitypubHttpBroadcastPayload, |
5 | ActivitypubHttpFetcherPayload, ActivitypubHttpUnicastPayload, EmailPayload, | 5 | ActivitypubHttpFetcherPayload, |
6 | ActivitypubHttpUnicastPayload, | ||
7 | EmailPayload, | ||
6 | JobState, | 8 | JobState, |
7 | JobType, RefreshPayload, VideoFileImportPayload, VideoImportPayload, VideoRedundancyPayload, VideoTranscodingPayload | 9 | JobType, |
10 | RefreshPayload, | ||
11 | VideoFileImportPayload, | ||
12 | VideoImportPayload, | ||
13 | VideoRedundancyPayload, | ||
14 | VideoTranscodingPayload | ||
8 | } from '../../../shared/models' | 15 | } from '../../../shared/models' |
9 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
10 | import { Redis } from '../redis' | 17 | import { Redis } from '../redis' |
@@ -13,13 +20,13 @@ import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-bro | |||
13 | import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' | 20 | import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' |
14 | import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' | 21 | import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' |
15 | import { processEmail } from './handlers/email' | 22 | import { processEmail } from './handlers/email' |
16 | import { processVideoTranscoding} from './handlers/video-transcoding' | 23 | import { processVideoTranscoding } from './handlers/video-transcoding' |
17 | import { processActivityPubFollow } from './handlers/activitypub-follow' | 24 | import { processActivityPubFollow } from './handlers/activitypub-follow' |
18 | import { processVideoImport} from './handlers/video-import' | 25 | import { processVideoImport } from './handlers/video-import' |
19 | import { processVideosViews } from './handlers/video-views' | 26 | import { processVideosViews } from './handlers/video-views' |
20 | import { refreshAPObject} from './handlers/activitypub-refresher' | 27 | import { refreshAPObject } from './handlers/activitypub-refresher' |
21 | import { processVideoFileImport} from './handlers/video-file-import' | 28 | import { processVideoFileImport } from './handlers/video-file-import' |
22 | import { processVideoRedundancy} from '@server/lib/job-queue/handlers/video-redundancy' | 29 | import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' |
23 | 30 | ||
24 | type CreateJobArgument = | 31 | type CreateJobArgument = |
25 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 32 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -117,7 +124,7 @@ class JobQueue { | |||
117 | 124 | ||
118 | createJob (obj: CreateJobArgument): void { | 125 | createJob (obj: CreateJobArgument): void { |
119 | this.createJobWithPromise(obj) | 126 | this.createJobWithPromise(obj) |
120 | .catch(err => logger.error('Cannot create job.', { err, obj })) | 127 | .catch(err => logger.error('Cannot create job.', { err, obj })) |
121 | } | 128 | } |
122 | 129 | ||
123 | createJobWithPromise (obj: CreateJobArgument) { | 130 | createJobWithPromise (obj: CreateJobArgument) { |
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index ea4a67802..7a6ed63be 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts | |||
@@ -14,6 +14,7 @@ import { MUser } from '@server/typings/models/user/user' | |||
14 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 14 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
15 | import { createUserAccountAndChannelAndPlaylist } from './user' | 15 | import { createUserAccountAndChannelAndPlaylist } from './user' |
16 | import { UserRole } from '@shared/models/users/user-role' | 16 | import { UserRole } from '@shared/models/users/user-role' |
17 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
17 | 18 | ||
18 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 19 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } |
19 | 20 | ||
@@ -82,7 +83,7 @@ async function getUser (usernameOrEmail: string, password: string) { | |||
82 | const obj = res.locals.bypassLogin | 83 | const obj = res.locals.bypassLogin |
83 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) | 84 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) |
84 | 85 | ||
85 | let user = await UserModel.loadByEmail(obj.user.username) | 86 | let user = await UserModel.loadByEmail(obj.user.email) |
86 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) | 87 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) |
87 | 88 | ||
88 | // This user does not belong to this plugin, skip it | 89 | // This user does not belong to this plugin, skip it |
@@ -94,7 +95,8 @@ async function getUser (usernameOrEmail: string, password: string) { | |||
94 | logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') | 95 | logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') |
95 | 96 | ||
96 | const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) | 97 | const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) |
97 | if (!user) return null | 98 | // If we don't find the user, or if the user belongs to a plugin |
99 | if (!user || user.pluginAuth !== null) return null | ||
98 | 100 | ||
99 | const passwordMatch = await user.isPasswordMatch(password) | 101 | const passwordMatch = await user.isPasswordMatch(password) |
100 | if (passwordMatch === false) return null | 102 | if (passwordMatch === false) return null |
@@ -109,8 +111,14 @@ async function getUser (usernameOrEmail: string, password: string) { | |||
109 | } | 111 | } |
110 | 112 | ||
111 | async function revokeToken (tokenInfo: TokenInfo) { | 113 | async function revokeToken (tokenInfo: TokenInfo) { |
114 | const res: express.Response = this.request.res | ||
112 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | 115 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) |
116 | |||
113 | if (token) { | 117 | if (token) { |
118 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | ||
119 | PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName) | ||
120 | } | ||
121 | |||
114 | clearCacheByToken(token.accessToken) | 122 | clearCacheByToken(token.accessToken) |
115 | 123 | ||
116 | token.destroy() | 124 | token.destroy() |
@@ -123,6 +131,12 @@ async function revokeToken (tokenInfo: TokenInfo) { | |||
123 | } | 131 | } |
124 | 132 | ||
125 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 133 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { |
134 | const res: express.Response = this.request.res | ||
135 | |||
136 | const authName = res.locals.bypassLogin?.bypass === true | ||
137 | ? res.locals.bypassLogin.authName | ||
138 | : null | ||
139 | |||
126 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | 140 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') |
127 | 141 | ||
128 | const tokenToCreate = { | 142 | const tokenToCreate = { |
@@ -130,6 +144,7 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User | |||
130 | accessTokenExpiresAt: token.accessTokenExpiresAt, | 144 | accessTokenExpiresAt: token.accessTokenExpiresAt, |
131 | refreshToken: token.refreshToken, | 145 | refreshToken: token.refreshToken, |
132 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, | 146 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, |
147 | authName, | ||
133 | oAuthClientId: client.id, | 148 | oAuthClientId: client.id, |
134 | userId: user.id | 149 | userId: user.id |
135 | } | 150 | } |
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index f78b989f5..9d646b689 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -76,7 +76,7 @@ export class PluginManager implements ServerHook { | |||
76 | return this.registeredPlugins[npmName] | 76 | return this.registeredPlugins[npmName] |
77 | } | 77 | } |
78 | 78 | ||
79 | getRegisteredPlugin (name: string) { | 79 | getRegisteredPluginByShortName (name: string) { |
80 | const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) | 80 | const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) |
81 | const registered = this.getRegisteredPluginOrTheme(npmName) | 81 | const registered = this.getRegisteredPluginOrTheme(npmName) |
82 | 82 | ||
@@ -85,7 +85,7 @@ export class PluginManager implements ServerHook { | |||
85 | return registered | 85 | return registered |
86 | } | 86 | } |
87 | 87 | ||
88 | getRegisteredTheme (name: string) { | 88 | getRegisteredThemeByShortName (name: string) { |
89 | const npmName = PluginModel.buildNpmName(name, PluginType.THEME) | 89 | const npmName = PluginModel.buildNpmName(name, PluginType.THEME) |
90 | const registered = this.getRegisteredPluginOrTheme(npmName) | 90 | const registered = this.getRegisteredPluginOrTheme(npmName) |
91 | 91 | ||
@@ -132,6 +132,22 @@ export class PluginManager implements ServerHook { | |||
132 | return this.translations[locale] || {} | 132 | return this.translations[locale] || {} |
133 | } | 133 | } |
134 | 134 | ||
135 | onLogout (npmName: string, authName: string) { | ||
136 | const plugin = this.getRegisteredPluginOrTheme(npmName) | ||
137 | if (!plugin || plugin.type !== PluginType.PLUGIN) return | ||
138 | |||
139 | const auth = plugin.registerHelpersStore.getIdAndPassAuths() | ||
140 | .find(a => a.authName === authName) | ||
141 | |||
142 | if (auth.onLogout) { | ||
143 | try { | ||
144 | auth.onLogout() | ||
145 | } catch (err) { | ||
146 | logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) | ||
147 | } | ||
148 | } | ||
149 | } | ||
150 | |||
135 | // ###################### Hooks ###################### | 151 | // ###################### Hooks ###################### |
136 | 152 | ||
137 | async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { | 153 | async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { |
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts index 7e827401f..679ed3650 100644 --- a/server/lib/plugins/register-helpers-store.ts +++ b/server/lib/plugins/register-helpers-store.ts | |||
@@ -171,6 +171,11 @@ export class RegisterHelpersStore { | |||
171 | 171 | ||
172 | private buildRegisterIdAndPassAuth () { | 172 | private buildRegisterIdAndPassAuth () { |
173 | return (options: RegisterServerAuthPassOptions) => { | 173 | return (options: RegisterServerAuthPassOptions) => { |
174 | if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') { | ||
175 | logger.error('Cannot register auth plugin %s: authName of getWeight or login are not valid.', this.npmName) | ||
176 | return | ||
177 | } | ||
178 | |||
174 | this.idAndPassAuths.push(options) | 179 | this.idAndPassAuths.push(options) |
175 | } | 180 | } |
176 | } | 181 | } |