diff options
author | Chocobozzz <me@florianbigard.com> | 2021-03-12 15:20:46 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-03-24 18:18:41 +0100 |
commit | f43db2f46ee50bacb402a6ef42d768694c2bc9a8 (patch) | |
tree | bce2574e94d48e8602387615a07ee691e98e23e4 | |
parent | cae2df6bdc3c3590df32bf7431a617177be30429 (diff) | |
download | PeerTube-f43db2f46ee50bacb402a6ef42d768694c2bc9a8.tar.gz PeerTube-f43db2f46ee50bacb402a6ef42d768694c2bc9a8.tar.zst PeerTube-f43db2f46ee50bacb402a6ef42d768694c2bc9a8.zip |
Refactor auth flow
Reimplement some node-oauth2-server methods to remove hacky code needed by our external
login workflow
24 files changed, 487 insertions, 255 deletions
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts index 3ddaffbdf..4fe3b964d 100644 --- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts +++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts | |||
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor { | |||
27 | catchError((err: HttpErrorResponse) => { | 27 | catchError((err: HttpErrorResponse) => { |
28 | if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { | 28 | if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { |
29 | return this.handleTokenExpired(req, next) | 29 | return this.handleTokenExpired(req, next) |
30 | } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) { | 30 | } |
31 | |||
32 | if (err.status === HttpStatusCode.UNAUTHORIZED_401) { | ||
31 | return this.handleNotAuthenticated(err) | 33 | return this.handleNotAuthenticated(err) |
32 | } | 34 | } |
33 | 35 | ||
diff --git a/package.json b/package.json index 323275faf..aba07173b 100644 --- a/package.json +++ b/package.json | |||
@@ -82,9 +82,6 @@ | |||
82 | "swagger-cli": "swagger-cli", | 82 | "swagger-cli": "swagger-cli", |
83 | "sass-lint": "sass-lint" | 83 | "sass-lint": "sass-lint" |
84 | }, | 84 | }, |
85 | "resolutions": { | ||
86 | "oauth2-server": "3.1.0-beta.1" | ||
87 | }, | ||
88 | "dependencies": { | 85 | "dependencies": { |
89 | "apicache": "1.6.2", | 86 | "apicache": "1.6.2", |
90 | "async": "^3.0.1", | 87 | "async": "^3.0.1", |
@@ -104,7 +101,6 @@ | |||
104 | "deep-object-diff": "^1.1.0", | 101 | "deep-object-diff": "^1.1.0", |
105 | "email-templates": "^8.0.3", | 102 | "email-templates": "^8.0.3", |
106 | "express": "^4.12.4", | 103 | "express": "^4.12.4", |
107 | "express-oauth-server": "^2.0.0", | ||
108 | "express-rate-limit": "^5.0.0", | 104 | "express-rate-limit": "^5.0.0", |
109 | "express-validator": "^6.4.0", | 105 | "express-validator": "^6.4.0", |
110 | "flat": "^5.0.0", | 106 | "flat": "^5.0.0", |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 3be1d55ae..e2b1ea7cd 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -2,8 +2,10 @@ import * as express from 'express' | |||
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { tokensRouter } from '@server/controllers/api/users/token' | 3 | import { tokensRouter } from '@server/controllers/api/users/token' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | ||
5 | import { MUser, MUserAccountDefault } from '@server/types/models' | 6 | import { MUser, MUserAccountDefault } from '@server/types/models' |
6 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | 7 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' |
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 9 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
8 | import { UserRegister } from '../../../../shared/models/users/user-register.model' | 10 | import { UserRegister } from '../../../../shared/models/users/user-register.model' |
9 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 11 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants' | |||
14 | import { sequelizeTypescript } from '../../../initializers/database' | 16 | import { sequelizeTypescript } from '../../../initializers/database' |
15 | import { Emailer } from '../../../lib/emailer' | 17 | import { Emailer } from '../../../lib/emailer' |
16 | import { Notifier } from '../../../lib/notifier' | 18 | import { Notifier } from '../../../lib/notifier' |
17 | import { deleteUserToken } from '../../../lib/oauth-model' | ||
18 | import { Redis } from '../../../lib/redis' | 19 | import { Redis } from '../../../lib/redis' |
19 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' | 20 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' |
20 | import { | 21 | import { |
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history' | |||
52 | import { myNotificationsRouter } from './my-notifications' | 53 | import { myNotificationsRouter } from './my-notifications' |
53 | import { mySubscriptionsRouter } from './my-subscriptions' | 54 | import { mySubscriptionsRouter } from './my-subscriptions' |
54 | import { myVideoPlaylistsRouter } from './my-video-playlists' | 55 | import { myVideoPlaylistsRouter } from './my-video-playlists' |
55 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
56 | 56 | ||
57 | const auditLogger = auditLoggerFactory('users') | 57 | const auditLogger = auditLoggerFactory('users') |
58 | 58 | ||
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) { | |||
335 | const user = await userToUpdate.save() | 335 | const user = await userToUpdate.save() |
336 | 336 | ||
337 | // Destroy user token to refresh rights | 337 | // Destroy user token to refresh rights |
338 | if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) | 338 | if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id) |
339 | 339 | ||
340 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) | 340 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) |
341 | 341 | ||
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault | |||
395 | user.blockedReason = reason || null | 395 | user.blockedReason = reason || null |
396 | 396 | ||
397 | await sequelizeTypescript.transaction(async t => { | 397 | await sequelizeTypescript.transaction(async t => { |
398 | await deleteUserToken(user.id, t) | 398 | await OAuthTokenModel.deleteUserToken(user.id, t) |
399 | 399 | ||
400 | await user.save({ transaction: t }) | 400 | await user.save({ transaction: t }) |
401 | }) | 401 | }) |
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index 821429358..3eae28b34 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts | |||
@@ -1,11 +1,14 @@ | |||
1 | import { handleLogin, handleTokenRevocation } from '@server/lib/auth' | 1 | import * as express from 'express' |
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { logger } from '@server/helpers/logger' | ||
3 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
4 | import * as express from 'express' | 6 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' |
7 | import { handleOAuthToken } from '@server/lib/auth/oauth' | ||
8 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | 9 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { asyncMiddleware, authenticate } from '@server/middlewares' | 10 | import { asyncMiddleware, authenticate } from '@server/middlewares' |
7 | import { ScopedToken } from '@shared/models/users/user-scoped-token' | 11 | import { ScopedToken } from '@shared/models/users/user-scoped-token' |
8 | import { v4 as uuidv4 } from 'uuid' | ||
9 | 12 | ||
10 | const tokensRouter = express.Router() | 13 | const tokensRouter = express.Router() |
11 | 14 | ||
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({ | |||
16 | 19 | ||
17 | tokensRouter.post('/token', | 20 | tokensRouter.post('/token', |
18 | loginRateLimiter, | 21 | loginRateLimiter, |
19 | handleLogin, | 22 | asyncMiddleware(handleToken) |
20 | tokenSuccess | ||
21 | ) | 23 | ) |
22 | 24 | ||
23 | tokensRouter.post('/revoke-token', | 25 | tokensRouter.post('/revoke-token', |
@@ -42,10 +44,53 @@ export { | |||
42 | } | 44 | } |
43 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
44 | 46 | ||
45 | function tokenSuccess (req: express.Request) { | 47 | async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) { |
46 | const username = req.body.username | 48 | const grantType = req.body.grant_type |
49 | |||
50 | try { | ||
51 | const bypassLogin = await buildByPassLogin(req, grantType) | ||
52 | |||
53 | const refreshTokenAuthName = grantType === 'refresh_token' | ||
54 | ? await getAuthNameFromRefreshGrant(req.body.refresh_token) | ||
55 | : undefined | ||
56 | |||
57 | const options = { | ||
58 | refreshTokenAuthName, | ||
59 | bypassLogin | ||
60 | } | ||
61 | |||
62 | const token = await handleOAuthToken(req, options) | ||
63 | |||
64 | res.set('Cache-Control', 'no-store') | ||
65 | res.set('Pragma', 'no-cache') | ||
66 | |||
67 | Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip }) | ||
68 | |||
69 | return res.json({ | ||
70 | token_type: 'Bearer', | ||
47 | 71 | ||
48 | Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) | 72 | access_token: token.accessToken, |
73 | refresh_token: token.refreshToken, | ||
74 | |||
75 | expires_in: token.accessTokenExpiresIn, | ||
76 | refresh_token_expires_in: token.refreshTokenExpiresIn | ||
77 | }) | ||
78 | } catch (err) { | ||
79 | logger.warn('Login error', { err }) | ||
80 | |||
81 | return res.status(err.code || 400).json({ | ||
82 | code: err.name, | ||
83 | error: err.message | ||
84 | }) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
89 | const token = res.locals.oauth.token | ||
90 | |||
91 | const result = await revokeToken(token, true) | ||
92 | |||
93 | return res.json(result) | ||
49 | } | 94 | } |
50 | 95 | ||
51 | function getScopedTokens (req: express.Request, res: express.Response) { | 96 | function getScopedTokens (req: express.Request, res: express.Response) { |
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) { | |||
66 | feedToken: user.feedToken | 111 | feedToken: user.feedToken |
67 | } as ScopedToken) | 112 | } as ScopedToken) |
68 | } | 113 | } |
114 | |||
115 | async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> { | ||
116 | if (grantType !== 'password') return undefined | ||
117 | |||
118 | if (req.body.externalAuthToken) { | ||
119 | // Consistency with the getBypassFromPasswordGrant promise | ||
120 | return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken) | ||
121 | } | ||
122 | |||
123 | return getBypassFromPasswordGrant(req.body.username, req.body.password) | ||
124 | } | ||
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index 6a1ccc0bf..105f51518 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' | 3 | import { logger } from '@server/helpers/logger' |
5 | import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' | 4 | import { optionalAuthenticate } from '@server/middlewares/auth' |
6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
7 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
8 | import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' | 5 | import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
9 | import { PluginType } from '../../shared/models/plugins/plugin.type' | 7 | import { PluginType } from '../../shared/models/plugins/plugin.type' |
10 | import { isTestInstance } from '../helpers/core-utils' | 8 | import { isTestInstance } from '../helpers/core-utils' |
11 | import { logger } from '@server/helpers/logger' | 9 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' |
12 | import { optionalAuthenticate } from '@server/middlewares/oauth' | 10 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' |
11 | import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins' | ||
12 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
13 | 13 | ||
14 | const sendFileOptions = { | 14 | const sendFileOptions = { |
15 | maxAge: '30 days', | 15 | maxAge: '30 days', |
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts index dbd421a7b..80f5064b6 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,28 +1,16 @@ | |||
1 | |||
1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' |
2 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
3 | import { generateRandomString } from '@server/helpers/utils' | 4 | import { generateRandomString } from '@server/helpers/utils' |
4 | import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
5 | import { revokeToken } from '@server/lib/oauth-model' | ||
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
8 | import { UserRole } from '@shared/models' | ||
9 | import { | 8 | import { |
10 | RegisterServerAuthenticatedResult, | 9 | RegisterServerAuthenticatedResult, |
11 | RegisterServerAuthPassOptions, | 10 | RegisterServerAuthPassOptions, |
12 | RegisterServerExternalAuthenticatedResult | 11 | RegisterServerExternalAuthenticatedResult |
13 | } from '@server/types/plugins/register-server-auth.model' | 12 | } from '@server/types/plugins/register-server-auth.model' |
14 | import * as express from 'express' | 13 | import { UserRole } from '@shared/models' |
15 | import * as OAuthServer from 'express-oauth-server' | ||
16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
17 | |||
18 | const oAuthServer = new OAuthServer({ | ||
19 | useErrorHandler: true, | ||
20 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
21 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
22 | allowExtendedTokenAttributes: true, | ||
23 | continueMiddleware: true, | ||
24 | model: require('./oauth-model') | ||
25 | }) | ||
26 | 14 | ||
27 | // Token is the key, expiration date is the value | 15 | // Token is the key, expiration date is the value |
28 | const authBypassTokens = new Map<string, { | 16 | const authBypassTokens = new Map<string, { |
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, { | |||
37 | npmName: string | 25 | npmName: string |
38 | }>() | 26 | }>() |
39 | 27 | ||
40 | async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
41 | const grantType = req.body.grant_type | ||
42 | |||
43 | if (grantType === 'password') { | ||
44 | if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) | ||
45 | else await proxifyPasswordGrant(req, res) | ||
46 | } else if (grantType === 'refresh_token') { | ||
47 | await proxifyRefreshGrant(req, res) | ||
48 | } | ||
49 | |||
50 | return forwardTokenReq(req, res, next) | ||
51 | } | ||
52 | |||
53 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
54 | const token = res.locals.oauth.token | ||
55 | |||
56 | res.locals.explicitLogout = true | ||
57 | const result = await revokeToken(token) | ||
58 | |||
59 | // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released | ||
60 | // oAuthServer.revoke(req, res, err => { | ||
61 | // if (err) { | ||
62 | // logger.warn('Error in revoke token handler.', { err }) | ||
63 | // | ||
64 | // return res.status(err.status) | ||
65 | // .json({ | ||
66 | // error: err.message, | ||
67 | // code: err.name | ||
68 | // }) | ||
69 | // .end() | ||
70 | // } | ||
71 | // }) | ||
72 | |||
73 | return res.json(result) | ||
74 | } | ||
75 | |||
76 | async function onExternalUserAuthenticated (options: { | 28 | async function onExternalUserAuthenticated (options: { |
77 | npmName: string | 29 | npmName: string |
78 | authName: string | 30 | authName: string |
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: { | |||
107 | authName | 59 | authName |
108 | }) | 60 | }) |
109 | 61 | ||
110 | // Cleanup | 62 | // Cleanup expired tokens |
111 | const now = new Date() | 63 | const now = new Date() |
112 | for (const [ key, value ] of authBypassTokens) { | 64 | for (const [ key, value ] of authBypassTokens) { |
113 | if (value.expires.getTime() < now.getTime()) { | 65 | if (value.expires.getTime() < now.getTime()) { |
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: { | |||
118 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | 70 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) |
119 | } | 71 | } |
120 | 72 | ||
121 | // --------------------------------------------------------------------------- | 73 | async function getAuthNameFromRefreshGrant (refreshToken?: string) { |
122 | 74 | if (!refreshToken) return undefined | |
123 | export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation } | ||
124 | |||
125 | // --------------------------------------------------------------------------- | ||
126 | |||
127 | function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) { | ||
128 | return oAuthServer.token()(req, res, err => { | ||
129 | if (err) { | ||
130 | logger.warn('Login error.', { err }) | ||
131 | |||
132 | return res.status(err.status) | ||
133 | .json({ | ||
134 | error: err.message, | ||
135 | code: err.name | ||
136 | }) | ||
137 | } | ||
138 | |||
139 | if (next) return next() | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | async function proxifyRefreshGrant (req: express.Request, res: express.Response) { | ||
144 | const refreshToken = req.body.refresh_token | ||
145 | if (!refreshToken) return | ||
146 | 75 | ||
147 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | 76 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) |
148 | if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName | 77 | |
78 | return tokenModel?.authName | ||
149 | } | 79 | } |
150 | 80 | ||
151 | async function proxifyPasswordGrant (req: express.Request, res: express.Response) { | 81 | async function getBypassFromPasswordGrant (username: string, password: string) { |
152 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 82 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
153 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
154 | 84 | ||
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
174 | }) | 104 | }) |
175 | 105 | ||
176 | const loginOptions = { | 106 | const loginOptions = { |
177 | id: req.body.username, | 107 | id: username, |
178 | password: req.body.password | 108 | password |
179 | } | 109 | } |
180 | 110 | ||
181 | for (const pluginAuth of pluginAuths) { | 111 | for (const pluginAuth of pluginAuths) { |
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
199 | authName, npmName, loginOptions.id | 129 | authName, npmName, loginOptions.id |
200 | ) | 130 | ) |
201 | 131 | ||
202 | res.locals.bypassLogin = { | 132 | return { |
203 | bypass: true, | 133 | bypass: true, |
204 | pluginName: pluginAuth.npmName, | 134 | pluginName: pluginAuth.npmName, |
205 | authName: authOptions.authName, | 135 | authName: authOptions.authName, |
206 | user: buildUserResult(loginResult) | 136 | user: buildUserResult(loginResult) |
207 | } | 137 | } |
208 | |||
209 | return | ||
210 | } catch (err) { | 138 | } catch (err) { |
211 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 139 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
212 | } | 140 | } |
213 | } | 141 | } |
142 | |||
143 | return undefined | ||
214 | } | 144 | } |
215 | 145 | ||
216 | function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { | 146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { |
217 | const obj = authBypassTokens.get(req.body.externalAuthToken) | 147 | const obj = authBypassTokens.get(externalAuthToken) |
218 | if (!obj) { | 148 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
219 | logger.error('Cannot authenticate user with unknown bypass token') | ||
220 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
221 | } | ||
222 | 149 | ||
223 | const { expires, user, authName, npmName } = obj | 150 | const { expires, user, authName, npmName } = obj |
224 | 151 | ||
225 | const now = new Date() | 152 | const now = new Date() |
226 | if (now.getTime() > expires.getTime()) { | 153 | if (now.getTime() > expires.getTime()) { |
227 | logger.error('Cannot authenticate user with an expired external auth token') | 154 | throw new Error('Cannot authenticate user with an expired external auth token') |
228 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
229 | } | 155 | } |
230 | 156 | ||
231 | if (user.username !== req.body.username) { | 157 | if (user.username !== username) { |
232 | logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) | 158 | throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`) |
233 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
234 | } | 159 | } |
235 | 160 | ||
236 | // Bypass oauth library validation | ||
237 | req.body.password = 'fake' | ||
238 | |||
239 | logger.info( | 161 | logger.info( |
240 | 'Auth success with external auth method %s of plugin %s for %s.', | 162 | 'Auth success with external auth method %s of plugin %s for %s.', |
241 | authName, npmName, user.email | 163 | authName, npmName, user.email |
242 | ) | 164 | ) |
243 | 165 | ||
244 | res.locals.bypassLogin = { | 166 | return { |
245 | bypass: true, | 167 | bypass: true, |
246 | pluginName: npmName, | 168 | pluginName: npmName, |
247 | authName: authName, | 169 | authName: authName, |
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
286 | displayName: pluginResult.displayName || pluginResult.username | 208 | displayName: pluginResult.displayName || pluginResult.username |
287 | } | 209 | } |
288 | } | 210 | } |
211 | |||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
214 | export { | ||
215 | onExternalUserAuthenticated, | ||
216 | getBypassFromExternalAuth, | ||
217 | getAuthNameFromRefreshGrant, | ||
218 | getBypassFromPasswordGrant | ||
219 | } | ||
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts index a2c53a2c9..c74869ee2 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,49 +1,35 @@ | |||
1 | import * as express from 'express' | ||
2 | import * as LRUCache from 'lru-cache' | ||
3 | import { AccessDeniedError } from 'oauth2-server' | 1 | import { AccessDeniedError } from 'oauth2-server' |
4 | import { Transaction } from 'sequelize' | ||
5 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 2 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
6 | import { ActorModel } from '@server/models/activitypub/actor' | 3 | import { ActorModel } from '@server/models/activitypub/actor' |
4 | import { MOAuthClient } from '@server/types/models' | ||
7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 5 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
8 | import { MUser } from '@server/types/models/user/user' | 6 | import { MUser } from '@server/types/models/user/user' |
9 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 7 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
10 | import { UserRole } from '@shared/models/users/user-role' | 8 | import { UserRole } from '@shared/models/users/user-role' |
11 | import { logger } from '../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../../initializers/config' |
13 | import { LRU_CACHE } from '../initializers/constants' | 11 | import { UserModel } from '../../models/account/user' |
14 | import { UserModel } from '../models/account/user' | 12 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
15 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 13 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
16 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 14 | import { createUserAccountAndChannelAndPlaylist } from '../user' |
17 | import { createUserAccountAndChannelAndPlaylist } from './user' | 15 | import { TokensCache } from './tokens-cache' |
18 | 16 | ||
19 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 17 | type TokenInfo = { |
20 | 18 | accessToken: string | |
21 | const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 19 | refreshToken: string |
22 | const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 20 | accessTokenExpiresAt: Date |
23 | 21 | refreshTokenExpiresAt: Date | |
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function deleteUserToken (userId: number, t?: Transaction) { | ||
27 | clearCacheByUserId(userId) | ||
28 | |||
29 | return OAuthTokenModel.deleteUserToken(userId, t) | ||
30 | } | 22 | } |
31 | 23 | ||
32 | function clearCacheByUserId (userId: number) { | 24 | export type BypassLogin = { |
33 | const token = userHavingToken.get(userId) | 25 | bypass: boolean |
34 | 26 | pluginName: string | |
35 | if (token !== undefined) { | 27 | authName?: string |
36 | accessTokenCache.del(token) | 28 | user: { |
37 | userHavingToken.del(userId) | 29 | username: string |
38 | } | 30 | email: string |
39 | } | 31 | displayName: string |
40 | 32 | role: UserRole | |
41 | function clearCacheByToken (token: string) { | ||
42 | const tokenModel = accessTokenCache.get(token) | ||
43 | |||
44 | if (tokenModel !== undefined) { | ||
45 | userHavingToken.del(tokenModel.userId) | ||
46 | accessTokenCache.del(token) | ||
47 | } | 33 | } |
48 | } | 34 | } |
49 | 35 | ||
@@ -54,15 +40,12 @@ async function getAccessToken (bearerToken: string) { | |||
54 | 40 | ||
55 | let tokenModel: MOAuthTokenUser | 41 | let tokenModel: MOAuthTokenUser |
56 | 42 | ||
57 | if (accessTokenCache.has(bearerToken)) { | 43 | if (TokensCache.Instance.hasToken(bearerToken)) { |
58 | tokenModel = accessTokenCache.get(bearerToken) | 44 | tokenModel = TokensCache.Instance.getByToken(bearerToken) |
59 | } else { | 45 | } else { |
60 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | 46 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) |
61 | 47 | ||
62 | if (tokenModel) { | 48 | if (tokenModel) TokensCache.Instance.setToken(tokenModel) |
63 | accessTokenCache.set(bearerToken, tokenModel) | ||
64 | userHavingToken.set(tokenModel.userId, tokenModel.accessToken) | ||
65 | } | ||
66 | } | 49 | } |
67 | 50 | ||
68 | if (!tokenModel) return undefined | 51 | if (!tokenModel) return undefined |
@@ -99,16 +82,13 @@ async function getRefreshToken (refreshToken: string) { | |||
99 | return tokenInfo | 82 | return tokenInfo |
100 | } | 83 | } |
101 | 84 | ||
102 | async function getUser (usernameOrEmail?: string, password?: string) { | 85 | async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) { |
103 | const res: express.Response = this.request.res | ||
104 | |||
105 | // Special treatment coming from a plugin | 86 | // Special treatment coming from a plugin |
106 | if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { | 87 | if (bypassLogin && bypassLogin.bypass === true) { |
107 | const obj = res.locals.bypassLogin | 88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
108 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) | ||
109 | 89 | ||
110 | let user = await UserModel.loadByEmail(obj.user.email) | 90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
111 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) | 91 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
112 | 92 | ||
113 | // Cannot create a user | 93 | // Cannot create a user |
114 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 94 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') |
@@ -117,7 +97,7 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
117 | // Then we just go through a regular login process | 97 | // Then we just go through a regular login process |
118 | if (user.pluginAuth !== null) { | 98 | if (user.pluginAuth !== null) { |
119 | // This user does not belong to this plugin, skip it | 99 | // This user does not belong to this plugin, skip it |
120 | if (user.pluginAuth !== obj.pluginName) return null | 100 | if (user.pluginAuth !== bypassLogin.pluginName) return null |
121 | 101 | ||
122 | checkUserValidityOrThrow(user) | 102 | checkUserValidityOrThrow(user) |
123 | 103 | ||
@@ -143,18 +123,20 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
143 | return user | 123 | return user |
144 | } | 124 | } |
145 | 125 | ||
146 | async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { | 126 | async function revokeToken ( |
147 | const res: express.Response = this.request.res | 127 | tokenInfo: { refreshToken: string }, |
128 | explicitLogout?: boolean | ||
129 | ): Promise<{ success: boolean, redirectUrl?: string }> { | ||
148 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | 130 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) |
149 | 131 | ||
150 | if (token) { | 132 | if (token) { |
151 | let redirectUrl: string | 133 | let redirectUrl: string |
152 | 134 | ||
153 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | 135 | if (explicitLogout === true && token.User.pluginAuth && token.authName) { |
154 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) | 136 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) |
155 | } | 137 | } |
156 | 138 | ||
157 | clearCacheByToken(token.accessToken) | 139 | TokensCache.Instance.clearCacheByToken(token.accessToken) |
158 | 140 | ||
159 | token.destroy() | 141 | token.destroy() |
160 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) | 142 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) |
@@ -165,14 +147,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ | |||
165 | return { success: false } | 147 | return { success: false } |
166 | } | 148 | } |
167 | 149 | ||
168 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 150 | async function saveToken ( |
169 | const res: express.Response = this.request.res | 151 | token: TokenInfo, |
170 | 152 | client: MOAuthClient, | |
153 | user: MUser, | ||
154 | options: { | ||
155 | refreshTokenAuthName?: string | ||
156 | bypassLogin?: BypassLogin | ||
157 | } = {} | ||
158 | ) { | ||
159 | const { refreshTokenAuthName, bypassLogin } = options | ||
171 | let authName: string = null | 160 | let authName: string = null |
172 | if (res.locals.bypassLogin?.bypass === true) { | 161 | |
173 | authName = res.locals.bypassLogin.authName | 162 | if (bypassLogin?.bypass === true) { |
174 | } else if (res.locals.refreshTokenAuthName) { | 163 | authName = bypassLogin.authName |
175 | authName = res.locals.refreshTokenAuthName | 164 | } else if (refreshTokenAuthName) { |
165 | authName = refreshTokenAuthName | ||
176 | } | 166 | } |
177 | 167 | ||
178 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | 168 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') |
@@ -199,17 +189,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User | |||
199 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, | 189 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, |
200 | client, | 190 | client, |
201 | user, | 191 | user, |
202 | refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) | 192 | accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt), |
193 | refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt) | ||
203 | } | 194 | } |
204 | } | 195 | } |
205 | 196 | ||
206 | // --------------------------------------------------------------------------- | ||
207 | |||
208 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
209 | export { | 197 | export { |
210 | deleteUserToken, | ||
211 | clearCacheByUserId, | ||
212 | clearCacheByToken, | ||
213 | getAccessToken, | 198 | getAccessToken, |
214 | getClient, | 199 | getClient, |
215 | getRefreshToken, | 200 | getRefreshToken, |
@@ -218,6 +203,8 @@ export { | |||
218 | saveToken | 203 | saveToken |
219 | } | 204 | } |
220 | 205 | ||
206 | // --------------------------------------------------------------------------- | ||
207 | |||
221 | async function createUserFromExternal (pluginAuth: string, options: { | 208 | async function createUserFromExternal (pluginAuth: string, options: { |
222 | username: string | 209 | username: string |
223 | email: string | 210 | email: string |
@@ -252,3 +239,7 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
252 | function checkUserValidityOrThrow (user: MUser) { | 239 | function checkUserValidityOrThrow (user: MUser) { |
253 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 240 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
254 | } | 241 | } |
242 | |||
243 | function buildExpiresIn (expiresAt: Date) { | ||
244 | return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000) | ||
245 | } | ||
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts new file mode 100644 index 000000000..5b6130d56 --- /dev/null +++ b/server/lib/auth/oauth.ts | |||
@@ -0,0 +1,180 @@ | |||
1 | import * as express from 'express' | ||
2 | import { | ||
3 | InvalidClientError, | ||
4 | InvalidGrantError, | ||
5 | InvalidRequestError, | ||
6 | Request, | ||
7 | Response, | ||
8 | UnauthorizedClientError, | ||
9 | UnsupportedGrantTypeError | ||
10 | } from 'oauth2-server' | ||
11 | import { randomBytesPromise, sha1 } from '@server/helpers/core-utils' | ||
12 | import { MOAuthClient } from '@server/types/models' | ||
13 | import { OAUTH_LIFETIME } from '../../initializers/constants' | ||
14 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | ||
15 | |||
16 | /** | ||
17 | * | ||
18 | * Reimplement some functions of OAuth2Server to inject external auth methods | ||
19 | * | ||
20 | */ | ||
21 | |||
22 | const oAuthServer = new (require('oauth2-server'))({ | ||
23 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
24 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
25 | |||
26 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
27 | model: require('./oauth-model') | ||
28 | }) | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) { | ||
33 | const request = new Request(req) | ||
34 | const { refreshTokenAuthName, bypassLogin } = options | ||
35 | |||
36 | if (request.method !== 'POST') { | ||
37 | throw new InvalidRequestError('Invalid request: method must be POST') | ||
38 | } | ||
39 | |||
40 | if (!request.is([ 'application/x-www-form-urlencoded' ])) { | ||
41 | throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded') | ||
42 | } | ||
43 | |||
44 | const clientId = request.body.client_id | ||
45 | const clientSecret = request.body.client_secret | ||
46 | |||
47 | if (!clientId || !clientSecret) { | ||
48 | throw new InvalidClientError('Invalid client: cannot retrieve client credentials') | ||
49 | } | ||
50 | |||
51 | const client = await getClient(clientId, clientSecret) | ||
52 | if (!client) { | ||
53 | throw new InvalidClientError('Invalid client: client is invalid') | ||
54 | } | ||
55 | |||
56 | const grantType = request.body.grant_type | ||
57 | if (!grantType) { | ||
58 | throw new InvalidRequestError('Missing parameter: `grant_type`') | ||
59 | } | ||
60 | |||
61 | if (![ 'password', 'refresh_token' ].includes(grantType)) { | ||
62 | throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid') | ||
63 | } | ||
64 | |||
65 | if (!client.grants.includes(grantType)) { | ||
66 | throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid') | ||
67 | } | ||
68 | |||
69 | if (grantType === 'password') { | ||
70 | return handlePasswordGrant({ | ||
71 | request, | ||
72 | client, | ||
73 | bypassLogin | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | return handleRefreshGrant({ | ||
78 | request, | ||
79 | client, | ||
80 | refreshTokenAuthName | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | async function handleOAuthAuthenticate ( | ||
85 | req: express.Request, | ||
86 | res: express.Response, | ||
87 | authenticateInQuery = false | ||
88 | ) { | ||
89 | const options = authenticateInQuery | ||
90 | ? { allowBearerTokensInQueryString: true } | ||
91 | : {} | ||
92 | |||
93 | return oAuthServer.authenticate(new Request(req), new Response(res), options) | ||
94 | } | ||
95 | |||
96 | export { | ||
97 | handleOAuthToken, | ||
98 | handleOAuthAuthenticate | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | async function handlePasswordGrant (options: { | ||
104 | request: Request | ||
105 | client: MOAuthClient | ||
106 | bypassLogin?: BypassLogin | ||
107 | }) { | ||
108 | const { request, client, bypassLogin } = options | ||
109 | |||
110 | if (!request.body.username) { | ||
111 | throw new InvalidRequestError('Missing parameter: `username`') | ||
112 | } | ||
113 | |||
114 | if (!bypassLogin && !request.body.password) { | ||
115 | throw new InvalidRequestError('Missing parameter: `password`') | ||
116 | } | ||
117 | |||
118 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | ||
119 | if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') | ||
120 | |||
121 | const token = await buildToken() | ||
122 | |||
123 | return saveToken(token, client, user, { bypassLogin }) | ||
124 | } | ||
125 | |||
126 | async function handleRefreshGrant (options: { | ||
127 | request: Request | ||
128 | client: MOAuthClient | ||
129 | refreshTokenAuthName: string | ||
130 | }) { | ||
131 | const { request, client, refreshTokenAuthName } = options | ||
132 | |||
133 | if (!request.body.refresh_token) { | ||
134 | throw new InvalidRequestError('Missing parameter: `refresh_token`') | ||
135 | } | ||
136 | |||
137 | const refreshToken = await getRefreshToken(request.body.refresh_token) | ||
138 | |||
139 | if (!refreshToken) { | ||
140 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
141 | } | ||
142 | |||
143 | if (refreshToken.client.id !== client.id) { | ||
144 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
145 | } | ||
146 | |||
147 | if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) { | ||
148 | throw new InvalidGrantError('Invalid grant: refresh token has expired') | ||
149 | } | ||
150 | |||
151 | await revokeToken({ refreshToken: refreshToken.refreshToken }) | ||
152 | |||
153 | const token = await buildToken() | ||
154 | |||
155 | return saveToken(token, client, refreshToken.user, { refreshTokenAuthName }) | ||
156 | } | ||
157 | |||
158 | function generateRandomToken () { | ||
159 | return randomBytesPromise(256) | ||
160 | .then(buffer => sha1(buffer)) | ||
161 | } | ||
162 | |||
163 | function getTokenExpiresAt (type: 'access' | 'refresh') { | ||
164 | const lifetime = type === 'access' | ||
165 | ? OAUTH_LIFETIME.ACCESS_TOKEN | ||
166 | : OAUTH_LIFETIME.REFRESH_TOKEN | ||
167 | |||
168 | return new Date(Date.now() + lifetime * 1000) | ||
169 | } | ||
170 | |||
171 | async function buildToken () { | ||
172 | const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ]) | ||
173 | |||
174 | return { | ||
175 | accessToken, | ||
176 | refreshToken, | ||
177 | accessTokenExpiresAt: getTokenExpiresAt('access'), | ||
178 | refreshTokenExpiresAt: getTokenExpiresAt('refresh') | ||
179 | } | ||
180 | } | ||
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts new file mode 100644 index 000000000..b027ce69a --- /dev/null +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as LRUCache from 'lru-cache' | ||
2 | import { MOAuthTokenUser } from '@server/types/models' | ||
3 | import { LRU_CACHE } from '../../initializers/constants' | ||
4 | |||
5 | export class TokensCache { | ||
6 | |||
7 | private static instance: TokensCache | ||
8 | |||
9 | private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
10 | private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
11 | |||
12 | private constructor () { } | ||
13 | |||
14 | static get Instance () { | ||
15 | return this.instance || (this.instance = new this()) | ||
16 | } | ||
17 | |||
18 | hasToken (token: string) { | ||
19 | return this.accessTokenCache.has(token) | ||
20 | } | ||
21 | |||
22 | getByToken (token: string) { | ||
23 | return this.accessTokenCache.get(token) | ||
24 | } | ||
25 | |||
26 | setToken (token: MOAuthTokenUser) { | ||
27 | this.accessTokenCache.set(token.accessToken, token) | ||
28 | this.userHavingToken.set(token.userId, token.accessToken) | ||
29 | } | ||
30 | |||
31 | deleteUserToken (userId: number) { | ||
32 | this.clearCacheByUserId(userId) | ||
33 | } | ||
34 | |||
35 | clearCacheByUserId (userId: number) { | ||
36 | const token = this.userHavingToken.get(userId) | ||
37 | |||
38 | if (token !== undefined) { | ||
39 | this.accessTokenCache.del(token) | ||
40 | this.userHavingToken.del(userId) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | clearCacheByToken (token: string) { | ||
45 | const tokenModel = this.accessTokenCache.get(token) | ||
46 | |||
47 | if (tokenModel !== undefined) { | ||
48 | this.userHavingToken.del(tokenModel.userId) | ||
49 | this.accessTokenCache.del(token) | ||
50 | } | ||
51 | } | ||
52 | } | ||
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index 1f2a88c27..9b5e1a546 100644 --- a/server/lib/plugins/register-helpers.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | VIDEO_PLAYLIST_PRIVACIES, | 7 | VIDEO_PLAYLIST_PRIVACIES, |
8 | VIDEO_PRIVACIES | 8 | VIDEO_PRIVACIES |
9 | } from '@server/initializers/constants' | 9 | } from '@server/initializers/constants' |
10 | import { onExternalUserAuthenticated } from '@server/lib/auth' | 10 | import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' |
11 | import { PluginModel } from '@server/models/server/plugin' | 11 | import { PluginModel } from '@server/models/server/plugin' |
12 | import { | 12 | import { |
13 | RegisterServerAuthExternalOptions, | 13 | RegisterServerAuthExternalOptions, |
diff --git a/server/middlewares/oauth.ts b/server/middlewares/auth.ts index 280595acc..f38373624 100644 --- a/server/middlewares/oauth.ts +++ b/server/middlewares/auth.ts | |||
@@ -1,15 +1,19 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { Socket } from 'socket.io' | 2 | import { Socket } from 'socket.io' |
3 | import { oAuthServer } from '@server/lib/auth' | 3 | import { getAccessToken } from '@server/lib/auth/oauth-model' |
4 | import { logger } from '../helpers/logger' | ||
5 | import { getAccessToken } from '../lib/oauth-model' | ||
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
5 | import { logger } from '../helpers/logger' | ||
6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | ||
7 | 7 | ||
8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { | 8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { |
9 | const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} | 9 | handleOAuthAuthenticate(req, res, authenticateInQuery) |
10 | .then((token: any) => { | ||
11 | res.locals.oauth = { token } | ||
12 | res.locals.authenticated = true | ||
10 | 13 | ||
11 | oAuthServer.authenticate(options)(req, res, err => { | 14 | return next() |
12 | if (err) { | 15 | }) |
16 | .catch(err => { | ||
13 | logger.warn('Cannot authenticate.', { err }) | 17 | logger.warn('Cannot authenticate.', { err }) |
14 | 18 | ||
15 | return res.status(err.status) | 19 | return res.status(err.status) |
@@ -17,13 +21,7 @@ function authenticate (req: express.Request, res: express.Response, next: expres | |||
17 | error: 'Token is invalid.', | 21 | error: 'Token is invalid.', |
18 | code: err.name | 22 | code: err.name |
19 | }) | 23 | }) |
20 | .end() | 24 | }) |
21 | } | ||
22 | |||
23 | res.locals.authenticated = true | ||
24 | |||
25 | return next() | ||
26 | }) | ||
27 | } | 25 | } |
28 | 26 | ||
29 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { | 27 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { |
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index b758a8586..3e280e16f 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | export * from './validators' | 1 | export * from './validators' |
2 | export * from './activitypub' | 2 | export * from './activitypub' |
3 | export * from './async' | 3 | export * from './async' |
4 | export * from './oauth' | 4 | export * from './auth' |
5 | export * from './pagination' | 5 | export * from './pagination' |
6 | export * from './servers' | 6 | export * from './servers' |
7 | export * from './sort' | 7 | export * from './sort' |
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 0fba4f5fd..c872d045e 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -29,7 +29,7 @@ import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoP | |||
29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' | 30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' |
31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' | 31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' |
32 | import { authenticatePromiseIfNeeded } from '../../oauth' | 32 | import { authenticatePromiseIfNeeded } from '../../auth' |
33 | import { areValidationErrors } from '../utils' | 33 | import { areValidationErrors } from '../utils' |
34 | 34 | ||
35 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ | 35 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 37cc07b94..4d31d3dcb 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -54,7 +54,7 @@ import { isLocalVideoAccepted } from '../../../lib/moderation' | |||
54 | import { Hooks } from '../../../lib/plugins/hooks' | 54 | import { Hooks } from '../../../lib/plugins/hooks' |
55 | import { AccountModel } from '../../../models/account/account' | 55 | import { AccountModel } from '../../../models/account/account' |
56 | import { VideoModel } from '../../../models/video/video' | 56 | import { VideoModel } from '../../../models/video/video' |
57 | import { authenticatePromiseIfNeeded } from '../../oauth' | 57 | import { authenticatePromiseIfNeeded } from '../../auth' |
58 | import { areValidationErrors } from '../utils' | 58 | import { areValidationErrors } from '../utils' |
59 | 59 | ||
60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ | 60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ |
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index de1501299..138051528 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts | |||
@@ -12,10 +12,10 @@ import { | |||
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
15 | import { MNotificationSettingFormattable } from '@server/types/models' | 16 | import { MNotificationSettingFormattable } from '@server/types/models' |
16 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 17 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 18 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
18 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
19 | import { throwIfNotValid } from '../utils' | 19 | import { throwIfNotValid } from '../utils' |
20 | import { UserModel } from './user' | 20 | import { UserModel } from './user' |
21 | 21 | ||
@@ -195,7 +195,7 @@ export class UserNotificationSettingModel extends Model { | |||
195 | @AfterUpdate | 195 | @AfterUpdate |
196 | @AfterDestroy | 196 | @AfterDestroy |
197 | static removeTokenCache (instance: UserNotificationSettingModel) { | 197 | static removeTokenCache (instance: UserNotificationSettingModel) { |
198 | return clearCacheByUserId(instance.userId) | 198 | return TokensCache.Instance.clearCacheByUserId(instance.userId) |
199 | } | 199 | } |
200 | 200 | ||
201 | toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { | 201 | toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index c1f22b76a..a7a65c489 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | Table, | 21 | Table, |
22 | UpdatedAt | 22 | UpdatedAt |
23 | } from 'sequelize-typescript' | 23 | } from 'sequelize-typescript' |
24 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
24 | import { | 25 | import { |
25 | MMyUserFormattable, | 26 | MMyUserFormattable, |
26 | MUser, | 27 | MUser, |
@@ -58,7 +59,6 @@ import { | |||
58 | } from '../../helpers/custom-validators/users' | 59 | } from '../../helpers/custom-validators/users' |
59 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 60 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
60 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 61 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
61 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
63 | import { ActorModel } from '../activitypub/actor' | 63 | import { ActorModel } from '../activitypub/actor' |
64 | import { ActorFollowModel } from '../activitypub/actor-follow' | 64 | import { ActorFollowModel } from '../activitypub/actor-follow' |
@@ -411,7 +411,7 @@ export class UserModel extends Model { | |||
411 | @AfterUpdate | 411 | @AfterUpdate |
412 | @AfterDestroy | 412 | @AfterDestroy |
413 | static removeTokenCache (instance: UserModel) { | 413 | static removeTokenCache (instance: UserModel) { |
414 | return clearCacheByUserId(instance.id) | 414 | return TokensCache.Instance.clearCacheByUserId(instance.id) |
415 | } | 415 | } |
416 | 416 | ||
417 | static countTotal () { | 417 | static countTotal () { |
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 6bc6cf27c..27e643aa7 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -12,9 +12,10 @@ import { | |||
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
16 | import { MUserAccountId } from '@server/types/models' | ||
15 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
16 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
17 | import { clearCacheByToken } from '../../lib/oauth-model' | ||
18 | import { AccountModel } from '../account/account' | 19 | import { AccountModel } from '../account/account' |
19 | import { UserModel } from '../account/user' | 20 | import { UserModel } from '../account/user' |
20 | import { ActorModel } from '../activitypub/actor' | 21 | import { ActorModel } from '../activitypub/actor' |
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = { | |||
26 | client: { | 27 | client: { |
27 | id: number | 28 | id: number |
28 | } | 29 | } |
29 | user: { | 30 | user: MUserAccountId |
30 | id: number | ||
31 | } | ||
32 | token: MOAuthTokenUser | 31 | token: MOAuthTokenUser |
33 | } | 32 | } |
34 | 33 | ||
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model { | |||
133 | @AfterUpdate | 132 | @AfterUpdate |
134 | @AfterDestroy | 133 | @AfterDestroy |
135 | static removeTokenCache (token: OAuthTokenModel) { | 134 | static removeTokenCache (token: OAuthTokenModel) { |
136 | return clearCacheByToken(token.accessToken) | 135 | return TokensCache.Instance.clearCacheByToken(token.accessToken) |
137 | } | 136 | } |
138 | 137 | ||
139 | static loadByRefreshToken (refreshToken: string) { | 138 | static loadByRefreshToken (refreshToken: string) { |
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model { | |||
206 | } | 205 | } |
207 | 206 | ||
208 | static deleteUserToken (userId: number, t?: Transaction) { | 207 | static deleteUserToken (userId: number, t?: Transaction) { |
208 | TokensCache.Instance.deleteUserToken(userId) | ||
209 | |||
209 | const query = { | 210 | const query = { |
210 | where: { | 211 | where: { |
211 | userId | 212 | userId |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 0a13f5b67..2b03fde2d 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -241,7 +241,7 @@ describe('Test users API validators', function () { | |||
241 | }) | 241 | }) |
242 | 242 | ||
243 | it('Should succeed with no password on a server with smtp enabled', async function () { | 243 | it('Should succeed with no password on a server with smtp enabled', async function () { |
244 | this.timeout(10000) | 244 | this.timeout(20000) |
245 | 245 | ||
246 | killallServers([ server ]) | 246 | killallServers([ server ]) |
247 | 247 | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 62a59033f..cea98aac7 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -4,10 +4,12 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' |
6 | import { CustomConfig } from '@shared/models/server' | 6 | import { CustomConfig } from '@shared/models/server' |
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { | 8 | import { |
8 | addVideoCommentThread, | 9 | addVideoCommentThread, |
9 | blockUser, | 10 | blockUser, |
10 | cleanupTests, | 11 | cleanupTests, |
12 | closeAllSequelize, | ||
11 | createUser, | 13 | createUser, |
12 | deleteMe, | 14 | deleteMe, |
13 | flushAndRunServer, | 15 | flushAndRunServer, |
@@ -24,6 +26,7 @@ import { | |||
24 | getVideoChannel, | 26 | getVideoChannel, |
25 | getVideosList, | 27 | getVideosList, |
26 | installPlugin, | 28 | installPlugin, |
29 | killallServers, | ||
27 | login, | 30 | login, |
28 | makePutBodyRequest, | 31 | makePutBodyRequest, |
29 | rateVideo, | 32 | rateVideo, |
@@ -31,7 +34,9 @@ import { | |||
31 | removeUser, | 34 | removeUser, |
32 | removeVideo, | 35 | removeVideo, |
33 | reportAbuse, | 36 | reportAbuse, |
37 | reRunServer, | ||
34 | ServerInfo, | 38 | ServerInfo, |
39 | setTokenField, | ||
35 | testImage, | 40 | testImage, |
36 | unblockUser, | 41 | unblockUser, |
37 | updateAbuse, | 42 | updateAbuse, |
@@ -44,10 +49,9 @@ import { | |||
44 | waitJobs | 49 | waitJobs |
45 | } from '../../../../shared/extra-utils' | 50 | } from '../../../../shared/extra-utils' |
46 | import { follow } from '../../../../shared/extra-utils/server/follows' | 51 | import { follow } from '../../../../shared/extra-utils/server/follows' |
47 | import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 52 | import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
48 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' | 53 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' |
49 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 54 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
50 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
51 | 55 | ||
52 | const expect = chai.expect | 56 | const expect = chai.expect |
53 | 57 | ||
@@ -89,6 +93,7 @@ describe('Test users', function () { | |||
89 | const client = { id: 'client', secret: server.client.secret } | 93 | const client = { id: 'client', secret: server.client.secret } |
90 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) | 94 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) |
91 | 95 | ||
96 | expect(res.body.code).to.equal('invalid_client') | ||
92 | expect(res.body.error).to.contain('client is invalid') | 97 | expect(res.body.error).to.contain('client is invalid') |
93 | }) | 98 | }) |
94 | 99 | ||
@@ -96,6 +101,7 @@ describe('Test users', function () { | |||
96 | const client = { id: server.client.id, secret: 'coucou' } | 101 | const client = { id: server.client.id, secret: 'coucou' } |
97 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) | 102 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) |
98 | 103 | ||
104 | expect(res.body.code).to.equal('invalid_client') | ||
99 | expect(res.body.error).to.contain('client is invalid') | 105 | expect(res.body.error).to.contain('client is invalid') |
100 | }) | 106 | }) |
101 | }) | 107 | }) |
@@ -106,6 +112,7 @@ describe('Test users', function () { | |||
106 | const user = { username: 'captain crochet', password: server.user.password } | 112 | const user = { username: 'captain crochet', password: server.user.password } |
107 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) | 113 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) |
108 | 114 | ||
115 | expect(res.body.code).to.equal('invalid_grant') | ||
109 | expect(res.body.error).to.contain('credentials are invalid') | 116 | expect(res.body.error).to.contain('credentials are invalid') |
110 | }) | 117 | }) |
111 | 118 | ||
@@ -113,6 +120,7 @@ describe('Test users', function () { | |||
113 | const user = { username: server.user.username, password: 'mew_three' } | 120 | const user = { username: server.user.username, password: 'mew_three' } |
114 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) | 121 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) |
115 | 122 | ||
123 | expect(res.body.code).to.equal('invalid_grant') | ||
116 | expect(res.body.error).to.contain('credentials are invalid') | 124 | expect(res.body.error).to.contain('credentials are invalid') |
117 | }) | 125 | }) |
118 | 126 | ||
@@ -245,12 +253,44 @@ describe('Test users', function () { | |||
245 | }) | 253 | }) |
246 | 254 | ||
247 | it('Should be able to login again', async function () { | 255 | it('Should be able to login again', async function () { |
248 | server.accessToken = await serverLogin(server) | 256 | const res = await login(server.url, server.client, server.user) |
257 | server.accessToken = res.body.access_token | ||
258 | server.refreshToken = res.body.refresh_token | ||
259 | }) | ||
260 | |||
261 | it('Should be able to get my user information again', async function () { | ||
262 | await getMyUserInformation(server.url, server.accessToken) | ||
263 | }) | ||
264 | |||
265 | it('Should have an expired access token', async function () { | ||
266 | this.timeout(15000) | ||
267 | |||
268 | await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
269 | await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
270 | |||
271 | killallServers([ server ]) | ||
272 | await reRunServer(server) | ||
273 | |||
274 | await getMyUserInformation(server.url, server.accessToken, 401) | ||
275 | }) | ||
276 | |||
277 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
278 | await refreshToken(server, server.refreshToken, 400) | ||
249 | }) | 279 | }) |
250 | 280 | ||
251 | it('Should have an expired access token') | 281 | it('Should refresh the token', async function () { |
282 | this.timeout(15000) | ||
283 | |||
284 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
285 | await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
252 | 286 | ||
253 | it('Should refresh the token') | 287 | killallServers([ server ]) |
288 | await reRunServer(server) | ||
289 | |||
290 | const res = await refreshToken(server, server.refreshToken) | ||
291 | server.accessToken = res.body.access_token | ||
292 | server.refreshToken = res.body.refresh_token | ||
293 | }) | ||
254 | 294 | ||
255 | it('Should be able to get my user information again', async function () { | 295 | it('Should be able to get my user information again', async function () { |
256 | await getMyUserInformation(server.url, server.accessToken) | 296 | await getMyUserInformation(server.url, server.accessToken) |
@@ -976,6 +1016,7 @@ describe('Test users', function () { | |||
976 | }) | 1016 | }) |
977 | 1017 | ||
978 | after(async function () { | 1018 | after(async function () { |
1019 | await closeAllSequelize([ server ]) | ||
979 | await cleanupTests([ server ]) | 1020 | await cleanupTests([ server ]) |
980 | }) | 1021 | }) |
981 | }) | 1022 | }) |
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts index a1b5e8f5d..5addb45c7 100644 --- a/server/tests/plugins/external-auth.ts +++ b/server/tests/plugins/external-auth.ts | |||
@@ -137,7 +137,7 @@ describe('Test external auth plugins', function () { | |||
137 | 137 | ||
138 | await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) | 138 | await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) |
139 | 139 | ||
140 | await waitUntilLog(server, 'expired external auth token') | 140 | await waitUntilLog(server, 'expired external auth token', 2) |
141 | }) | 141 | }) |
142 | 142 | ||
143 | it('Should auto login Cyan, create the user and use the token', async function () { | 143 | it('Should auto login Cyan, create the user and use the token', async function () { |
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 66acfb3f5..b0004dc7b 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -17,7 +17,6 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server' | |||
17 | import { MVideoImportDefault } from '@server/types/models/video/video-import' | 17 | import { MVideoImportDefault } from '@server/types/models/video/video-import' |
18 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' | 18 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' |
19 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' | 19 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' |
20 | import { UserRole } from '@shared/models' | ||
21 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' | 20 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' |
22 | import { | 21 | import { |
23 | MAccountDefault, | 22 | MAccountDefault, |
@@ -49,22 +48,6 @@ declare module 'express' { | |||
49 | } | 48 | } |
50 | 49 | ||
51 | interface PeerTubeLocals { | 50 | interface PeerTubeLocals { |
52 | bypassLogin?: { | ||
53 | bypass: boolean | ||
54 | pluginName: string | ||
55 | authName?: string | ||
56 | user: { | ||
57 | username: string | ||
58 | email: string | ||
59 | displayName: string | ||
60 | role: UserRole | ||
61 | } | ||
62 | } | ||
63 | |||
64 | refreshTokenAuthName?: string | ||
65 | |||
66 | explicitLogout?: boolean | ||
67 | |||
68 | videoAll?: MVideoFullLight | 51 | videoAll?: MVideoFullLight |
69 | onlyImmutableVideo?: MVideoImmutable | 52 | onlyImmutableVideo?: MVideoImmutable |
70 | onlyVideo?: MVideoThumbnail | 53 | onlyVideo?: MVideoThumbnail |
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts index 345e5bc16..35e493456 100644 --- a/shared/extra-utils/miscs/sql.ts +++ b/shared/extra-utils/miscs/sql.ts | |||
@@ -130,6 +130,14 @@ function setActorFollowScores (internalServerNumber: number, newScore: number) { | |||
130 | return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) | 130 | return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) |
131 | } | 131 | } |
132 | 132 | ||
133 | function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) { | ||
134 | const seq = getSequelize(internalServerNumber) | ||
135 | |||
136 | const options = { type: QueryTypes.UPDATE } | ||
137 | |||
138 | return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options) | ||
139 | } | ||
140 | |||
133 | export { | 141 | export { |
134 | setVideoField, | 142 | setVideoField, |
135 | setPlaylistField, | 143 | setPlaylistField, |
@@ -139,6 +147,7 @@ export { | |||
139 | setPluginLatestVersion, | 147 | setPluginLatestVersion, |
140 | selectQuery, | 148 | selectQuery, |
141 | deleteAll, | 149 | deleteAll, |
150 | setTokenField, | ||
142 | updateQuery, | 151 | updateQuery, |
143 | setActorFollowScores, | 152 | setActorFollowScores, |
144 | closeAllSequelize, | 153 | closeAllSequelize, |
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 08d05ef36..779a3cc36 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts | |||
@@ -37,6 +37,7 @@ interface ServerInfo { | |||
37 | customConfigFile?: string | 37 | customConfigFile?: string |
38 | 38 | ||
39 | accessToken?: string | 39 | accessToken?: string |
40 | refreshToken?: string | ||
40 | videoChannel?: VideoChannel | 41 | videoChannel?: VideoChannel |
41 | 42 | ||
42 | video?: { | 43 | video?: { |
@@ -1657,7 +1657,7 @@ bluebird@^2.10.0: | |||
1657 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" | 1657 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" |
1658 | integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= | 1658 | integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= |
1659 | 1659 | ||
1660 | bluebird@^3.0.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.7.2: | 1660 | bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.7.2: |
1661 | version "3.7.2" | 1661 | version "3.7.2" |
1662 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" | 1662 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" |
1663 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== | 1663 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== |
@@ -3401,15 +3401,6 @@ exif-parser@^0.1.12: | |||
3401 | resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" | 3401 | resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" |
3402 | integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI= | 3402 | integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI= |
3403 | 3403 | ||
3404 | express-oauth-server@^2.0.0: | ||
3405 | version "2.0.0" | ||
3406 | resolved "https://registry.yarnpkg.com/express-oauth-server/-/express-oauth-server-2.0.0.tgz#57b08665c1201532f52c4c02f19709238b99a48d" | ||
3407 | integrity sha1-V7CGZcEgFTL1LEwC8ZcJI4uZpI0= | ||
3408 | dependencies: | ||
3409 | bluebird "^3.0.5" | ||
3410 | express "^4.13.3" | ||
3411 | oauth2-server "3.0.0" | ||
3412 | |||
3413 | express-rate-limit@^5.0.0: | 3404 | express-rate-limit@^5.0.0: |
3414 | version "5.2.6" | 3405 | version "5.2.6" |
3415 | resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0" | 3406 | resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0" |
@@ -3423,7 +3414,7 @@ express-validator@^6.4.0: | |||
3423 | lodash "^4.17.20" | 3414 | lodash "^4.17.20" |
3424 | validator "^13.5.2" | 3415 | validator "^13.5.2" |
3425 | 3416 | ||
3426 | express@^4.12.4, express@^4.13.3, express@^4.16.4, express@^4.17.1: | 3417 | express@^4.12.4, express@^4.16.4, express@^4.17.1: |
3427 | version "4.17.1" | 3418 | version "4.17.1" |
3428 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" | 3419 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" |
3429 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== | 3420 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== |
@@ -5892,7 +5883,7 @@ oauth-sign@~0.9.0: | |||
5892 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" | 5883 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" |
5893 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== | 5884 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== |
5894 | 5885 | ||
5895 | oauth2-server@3.0.0, oauth2-server@3.1.0-beta.1: | 5886 | oauth2-server@3.1.0-beta.1: |
5896 | version "3.1.0-beta.1" | 5887 | version "3.1.0-beta.1" |
5897 | resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.0-beta.1.tgz#159ee4d32d148c2dc7a39f7b1ce872e039b91a41" | 5888 | resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.0-beta.1.tgz#159ee4d32d148c2dc7a39f7b1ce872e039b91a41" |
5898 | integrity sha512-FWLl/YC5NGvGzAtclhmlY9fG0nKwDP7xPiPOi5fZ4APO34BmF/vxsEp22spJNuSOrGEsp9W7jKtFCI3UBSvx5w== | 5889 | integrity sha512-FWLl/YC5NGvGzAtclhmlY9fG0nKwDP7xPiPOi5fZ4APO34BmF/vxsEp22spJNuSOrGEsp9W7jKtFCI3UBSvx5w== |