aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-03-12 15:20:46 +0100
committerChocobozzz <me@florianbigard.com>2021-03-24 18:18:41 +0100
commitf43db2f46ee50bacb402a6ef42d768694c2bc9a8 (patch)
treebce2574e94d48e8602387615a07ee691e98e23e4 /server/lib
parentcae2df6bdc3c3590df32bf7431a617177be30429 (diff)
downloadPeerTube-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
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/auth/external-auth.ts (renamed from server/lib/auth.ts)129
-rw-r--r--server/lib/auth/oauth-model.ts (renamed from server/lib/oauth-model.ts)131
-rw-r--r--server/lib/auth/oauth.ts180
-rw-r--r--server/lib/auth/tokens-cache.ts52
-rw-r--r--server/lib/plugins/register-helpers.ts2
5 files changed, 324 insertions, 170 deletions
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
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' 2import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils' 4import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' 5import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager' 6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import { 8import {
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'
14import * as express from 'express' 13import { UserRole } from '@shared/models'
15import * as OAuthServer from 'express-oauth-server'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17
18const 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
28const authBypassTokens = new Map<string, { 16const authBypassTokens = new Map<string, {
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, {
37 npmName: string 25 npmName: string
38}>() 26}>()
39 27
40async 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
53async 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
76async function onExternalUserAuthenticated (options: { 28async 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// --------------------------------------------------------------------------- 73async function getAuthNameFromRefreshGrant (refreshToken?: string) {
122 74 if (!refreshToken) return undefined
123export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
124
125// ---------------------------------------------------------------------------
126
127function 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
143async 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
151async function proxifyPasswordGrant (req: express.Request, res: express.Response) { 81async 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
216function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { 146function 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
214export {
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 @@
1import * as express from 'express'
2import * as LRUCache from 'lru-cache'
3import { AccessDeniedError } from 'oauth2-server' 1import { AccessDeniedError } from 'oauth2-server'
4import { Transaction } from 'sequelize'
5import { PluginManager } from '@server/lib/plugins/plugin-manager' 2import { PluginManager } from '@server/lib/plugins/plugin-manager'
6import { ActorModel } from '@server/models/activitypub/actor' 3import { ActorModel } from '@server/models/activitypub/actor'
4import { MOAuthClient } from '@server/types/models'
7import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 5import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
8import { MUser } from '@server/types/models/user/user' 6import { MUser } from '@server/types/models/user/user'
9import { UserAdminFlag } from '@shared/models/users/user-flag.model' 7import { UserAdminFlag } from '@shared/models/users/user-flag.model'
10import { UserRole } from '@shared/models/users/user-role' 8import { UserRole } from '@shared/models/users/user-role'
11import { logger } from '../helpers/logger' 9import { logger } from '../../helpers/logger'
12import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../../initializers/config'
13import { LRU_CACHE } from '../initializers/constants' 11import { UserModel } from '../../models/account/user'
14import { UserModel } from '../models/account/user' 12import { OAuthClientModel } from '../../models/oauth/oauth-client'
15import { OAuthClientModel } from '../models/oauth/oauth-client' 13import { OAuthTokenModel } from '../../models/oauth/oauth-token'
16import { OAuthTokenModel } from '../models/oauth/oauth-token' 14import { createUserAccountAndChannelAndPlaylist } from '../user'
17import { createUserAccountAndChannelAndPlaylist } from './user' 15import { TokensCache } from './tokens-cache'
18 16
19type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 17type TokenInfo = {
20 18 accessToken: string
21const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 19 refreshToken: string
22const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 20 accessTokenExpiresAt: Date
23 21 refreshTokenExpiresAt: Date
24// ---------------------------------------------------------------------------
25
26function deleteUserToken (userId: number, t?: Transaction) {
27 clearCacheByUserId(userId)
28
29 return OAuthTokenModel.deleteUserToken(userId, t)
30} 22}
31 23
32function clearCacheByUserId (userId: number) { 24export 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
41function 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
102async function getUser (usernameOrEmail?: string, password?: string) { 85async 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
146async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { 126async 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
168async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 150async 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
209export { 197export {
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
221async function createUserFromExternal (pluginAuth: string, options: { 208async 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: {
252function checkUserValidityOrThrow (user: MUser) { 239function 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
243function 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 @@
1import * as express from 'express'
2import {
3 InvalidClientError,
4 InvalidGrantError,
5 InvalidRequestError,
6 Request,
7 Response,
8 UnauthorizedClientError,
9 UnsupportedGrantTypeError
10} from 'oauth2-server'
11import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
12import { MOAuthClient } from '@server/types/models'
13import { OAUTH_LIFETIME } from '../../initializers/constants'
14import { 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
22const 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
32async 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
84async 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
96export {
97 handleOAuthToken,
98 handleOAuthAuthenticate
99}
100
101// ---------------------------------------------------------------------------
102
103async 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
126async 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
158function generateRandomToken () {
159 return randomBytesPromise(256)
160 .then(buffer => sha1(buffer))
161}
162
163function 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
171async 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 @@
1import * as LRUCache from 'lru-cache'
2import { MOAuthTokenUser } from '@server/types/models'
3import { LRU_CACHE } from '../../initializers/constants'
4
5export 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'
10import { onExternalUserAuthenticated } from '@server/lib/auth' 10import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
11import { PluginModel } from '@server/models/server/plugin' 11import { PluginModel } from '@server/models/server/plugin'
12import { 12import {
13 RegisterServerAuthExternalOptions, 13 RegisterServerAuthExternalOptions,