diff options
Diffstat (limited to 'server/lib/auth.ts')
-rw-r--r-- | server/lib/auth.ts | 225 |
1 files changed, 159 insertions, 66 deletions
diff --git a/server/lib/auth.ts b/server/lib/auth.ts index 5a6dd9dec..eaae5fdf3 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth.ts | |||
@@ -1,13 +1,18 @@ | |||
1 | import * as express from 'express' | 1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' |
2 | import { OAUTH_LIFETIME } from '@server/initializers/constants' | ||
3 | import * as OAuthServer from 'express-oauth-server' | ||
4 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
5 | import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model' | ||
6 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
7 | import { UserRole } from '@shared/models' | 3 | import { generateRandomString } from '@server/helpers/utils' |
4 | import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants' | ||
8 | import { revokeToken } from '@server/lib/oauth-model' | 5 | import { revokeToken } from '@server/lib/oauth-model' |
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
9 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
10 | import { isUserUsernameValid, isUserRoleValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users' | 8 | import { UserRole } from '@shared/models' |
9 | import { | ||
10 | RegisterServerAuthenticatedResult, | ||
11 | RegisterServerAuthPassOptions, | ||
12 | RegisterServerExternalAuthenticatedResult | ||
13 | } from '@shared/models/plugins/register-server-auth.model' | ||
14 | import * as express from 'express' | ||
15 | import * as OAuthServer from 'express-oauth-server' | ||
11 | 16 | ||
12 | const oAuthServer = new OAuthServer({ | 17 | const oAuthServer = new OAuthServer({ |
13 | useErrorHandler: true, | 18 | useErrorHandler: true, |
@@ -17,15 +22,28 @@ const oAuthServer = new OAuthServer({ | |||
17 | model: require('./oauth-model') | 22 | model: require('./oauth-model') |
18 | }) | 23 | }) |
19 | 24 | ||
20 | function onExternalAuthPlugin (npmName: string, username: string, email: string) { | 25 | // Token is the key, expiration date is the value |
21 | 26 | const authBypassTokens = new Map<string, { | |
22 | } | 27 | expires: Date |
28 | user: { | ||
29 | username: string | ||
30 | email: string | ||
31 | displayName: string | ||
32 | role: UserRole | ||
33 | } | ||
34 | authName: string | ||
35 | npmName: string | ||
36 | }>() | ||
23 | 37 | ||
24 | async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | 38 | async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { |
25 | const grantType = req.body.grant_type | 39 | const grantType = req.body.grant_type |
26 | 40 | ||
27 | if (grantType === 'password') await proxifyPasswordGrant(req, res) | 41 | if (grantType === 'password') { |
28 | else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res) | 42 | if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) |
43 | else await proxifyPasswordGrant(req, res) | ||
44 | } else if (grantType === 'refresh_token') { | ||
45 | await proxifyRefreshGrant(req, res) | ||
46 | } | ||
29 | 47 | ||
30 | return forwardTokenReq(req, res, next) | 48 | return forwardTokenReq(req, res, next) |
31 | } | 49 | } |
@@ -53,31 +71,60 @@ async function handleTokenRevocation (req: express.Request, res: express.Respons | |||
53 | return res.sendStatus(200) | 71 | return res.sendStatus(200) |
54 | } | 72 | } |
55 | 73 | ||
56 | // --------------------------------------------------------------------------- | 74 | async function onExternalUserAuthenticated (options: { |
75 | npmName: string | ||
76 | authName: string | ||
77 | authResult: RegisterServerExternalAuthenticatedResult | ||
78 | }) { | ||
79 | const { npmName, authName, authResult } = options | ||
57 | 80 | ||
58 | export { | 81 | if (!authResult.req || !authResult.res) { |
59 | oAuthServer, | 82 | logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName) |
60 | handleIdAndPassLogin, | 83 | return |
61 | onExternalAuthPlugin, | 84 | } |
62 | handleTokenRevocation | 85 | |
86 | if (!isAuthResultValid(npmName, authName, authResult)) return | ||
87 | |||
88 | const { res } = authResult | ||
89 | |||
90 | logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) | ||
91 | |||
92 | const bypassToken = await generateRandomString(32) | ||
93 | const tokenLifetime = 1000 * 60 * 5 // 5 minutes | ||
94 | |||
95 | const expires = new Date() | ||
96 | expires.setTime(expires.getTime() + tokenLifetime) | ||
97 | |||
98 | const user = buildUserResult(authResult) | ||
99 | authBypassTokens.set(bypassToken, { | ||
100 | expires, | ||
101 | user, | ||
102 | npmName, | ||
103 | authName | ||
104 | }) | ||
105 | |||
106 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | ||
63 | } | 107 | } |
64 | 108 | ||
65 | // --------------------------------------------------------------------------- | 109 | // --------------------------------------------------------------------------- |
66 | 110 | ||
67 | function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) { | 111 | export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, handleTokenRevocation } |
112 | |||
113 | // --------------------------------------------------------------------------- | ||
114 | |||
115 | function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) { | ||
68 | return oAuthServer.token()(req, res, err => { | 116 | return oAuthServer.token()(req, res, err => { |
69 | if (err) { | 117 | if (err) { |
70 | logger.warn('Login error.', { err }) | 118 | logger.warn('Login error.', { err }) |
71 | 119 | ||
72 | return res.status(err.status) | 120 | return res.status(err.status) |
73 | .json({ | 121 | .json({ |
74 | error: err.message, | 122 | error: err.message, |
75 | code: err.name | 123 | code: err.name |
76 | }) | 124 | }) |
77 | .end() | ||
78 | } | 125 | } |
79 | 126 | ||
80 | return next() | 127 | if (next) return next() |
81 | }) | 128 | }) |
82 | } | 129 | } |
83 | 130 | ||
@@ -131,50 +178,96 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
131 | 178 | ||
132 | try { | 179 | try { |
133 | const loginResult = await authOptions.login(loginOptions) | 180 | const loginResult = await authOptions.login(loginOptions) |
134 | if (loginResult) { | 181 | |
135 | logger.info( | 182 | if (!loginResult) continue |
136 | 'Login success with auth method %s of plugin %s for %s.', | 183 | if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue |
137 | authName, npmName, loginOptions.id | 184 | |
138 | ) | 185 | logger.info( |
139 | 186 | 'Login success with auth method %s of plugin %s for %s.', | |
140 | if (!isUserUsernameValid(loginResult.username)) { | 187 | authName, npmName, loginOptions.id |
141 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { loginResult }) | 188 | ) |
142 | continue | 189 | |
143 | } | 190 | res.locals.bypassLogin = { |
144 | 191 | bypass: true, | |
145 | if (!loginResult.email) { | 192 | pluginName: pluginAuth.npmName, |
146 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult }) | 193 | authName: authOptions.authName, |
147 | continue | 194 | user: buildUserResult(loginResult) |
148 | } | ||
149 | |||
150 | // role is optional | ||
151 | if (loginResult.role && !isUserRoleValid(loginResult.role)) { | ||
152 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { loginResult }) | ||
153 | continue | ||
154 | } | ||
155 | |||
156 | // display name is optional | ||
157 | if (loginResult.displayName && !isUserDisplayNameValid(loginResult.displayName)) { | ||
158 | logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { loginResult }) | ||
159 | continue | ||
160 | } | ||
161 | |||
162 | res.locals.bypassLogin = { | ||
163 | bypass: true, | ||
164 | pluginName: pluginAuth.npmName, | ||
165 | authName: authOptions.authName, | ||
166 | user: { | ||
167 | username: loginResult.username, | ||
168 | email: loginResult.email, | ||
169 | role: loginResult.role || UserRole.USER, | ||
170 | displayName: loginResult.displayName || loginResult.username | ||
171 | } | ||
172 | } | ||
173 | |||
174 | return | ||
175 | } | 195 | } |
196 | |||
197 | return | ||
176 | } catch (err) { | 198 | } catch (err) { |
177 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 199 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
178 | } | 200 | } |
179 | } | 201 | } |
180 | } | 202 | } |
203 | |||
204 | function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { | ||
205 | const obj = authBypassTokens.get(req.body.externalAuthToken) | ||
206 | if (!obj) { | ||
207 | logger.error('Cannot authenticate user with unknown bypass token') | ||
208 | return res.sendStatus(400) | ||
209 | } | ||
210 | |||
211 | const { expires, user, authName, npmName } = obj | ||
212 | |||
213 | const now = new Date() | ||
214 | if (now.getTime() > expires.getTime()) { | ||
215 | logger.error('Cannot authenticate user with an expired bypass token') | ||
216 | return res.sendStatus(400) | ||
217 | } | ||
218 | |||
219 | if (user.username !== req.body.username) { | ||
220 | logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) | ||
221 | return res.sendStatus(400) | ||
222 | } | ||
223 | |||
224 | // Bypass oauth library validation | ||
225 | req.body.password = 'fake' | ||
226 | |||
227 | logger.info( | ||
228 | 'Auth success with external auth method %s of plugin %s for %s.', | ||
229 | authName, npmName, user.email | ||
230 | ) | ||
231 | |||
232 | res.locals.bypassLogin = { | ||
233 | bypass: true, | ||
234 | pluginName: npmName, | ||
235 | authName: authName, | ||
236 | user | ||
237 | } | ||
238 | } | ||
239 | |||
240 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | ||
241 | if (!isUserUsernameValid(result.username)) { | ||
242 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { result }) | ||
243 | return false | ||
244 | } | ||
245 | |||
246 | if (!result.email) { | ||
247 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { result }) | ||
248 | return false | ||
249 | } | ||
250 | |||
251 | // role is optional | ||
252 | if (result.role && !isUserRoleValid(result.role)) { | ||
253 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { result }) | ||
254 | return false | ||
255 | } | ||
256 | |||
257 | // display name is optional | ||
258 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) { | ||
259 | logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { result }) | ||
260 | return false | ||
261 | } | ||
262 | |||
263 | return true | ||
264 | } | ||
265 | |||
266 | function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | ||
267 | return { | ||
268 | username: pluginResult.username, | ||
269 | email: pluginResult.email, | ||
270 | role: pluginResult.role || UserRole.USER, | ||
271 | displayName: pluginResult.displayName || pluginResult.username | ||
272 | } | ||
273 | } | ||