]>
Commit | Line | Data |
---|---|---|
1 | import * as express from 'express' | |
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' | |
7 | import { UserRole } from '@shared/models' | |
8 | import { revokeToken } from '@server/lib/oauth-model' | |
9 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | |
10 | import { isUserUsernameValid, isUserRoleValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users' | |
11 | ||
12 | const oAuthServer = new OAuthServer({ | |
13 | useErrorHandler: true, | |
14 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | |
15 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | |
16 | continueMiddleware: true, | |
17 | model: require('./oauth-model') | |
18 | }) | |
19 | ||
20 | function onExternalAuthPlugin (npmName: string, username: string, email: string) { | |
21 | ||
22 | } | |
23 | ||
24 | async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | |
25 | const grantType = req.body.grant_type | |
26 | ||
27 | if (grantType === 'password') await proxifyPasswordGrant(req, res) | |
28 | else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res) | |
29 | ||
30 | return forwardTokenReq(req, res, next) | |
31 | } | |
32 | ||
33 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | |
34 | const token = res.locals.oauth.token | |
35 | ||
36 | res.locals.explicitLogout = true | |
37 | await revokeToken(token) | |
38 | ||
39 | // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released | |
40 | // oAuthServer.revoke(req, res, err => { | |
41 | // if (err) { | |
42 | // logger.warn('Error in revoke token handler.', { err }) | |
43 | // | |
44 | // return res.status(err.status) | |
45 | // .json({ | |
46 | // error: err.message, | |
47 | // code: err.name | |
48 | // }) | |
49 | // .end() | |
50 | // } | |
51 | // }) | |
52 | ||
53 | return res.sendStatus(200) | |
54 | } | |
55 | ||
56 | // --------------------------------------------------------------------------- | |
57 | ||
58 | export { | |
59 | oAuthServer, | |
60 | handleIdAndPassLogin, | |
61 | onExternalAuthPlugin, | |
62 | handleTokenRevocation | |
63 | } | |
64 | ||
65 | // --------------------------------------------------------------------------- | |
66 | ||
67 | function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) { | |
68 | return oAuthServer.token()(req, res, err => { | |
69 | if (err) { | |
70 | logger.warn('Login error.', { err }) | |
71 | ||
72 | return res.status(err.status) | |
73 | .json({ | |
74 | error: err.message, | |
75 | code: err.name | |
76 | }) | |
77 | .end() | |
78 | } | |
79 | ||
80 | return next() | |
81 | }) | |
82 | } | |
83 | ||
84 | async function proxifyRefreshGrant (req: express.Request, res: express.Response) { | |
85 | const refreshToken = req.body.refresh_token | |
86 | if (!refreshToken) return | |
87 | ||
88 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | |
89 | if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName | |
90 | } | |
91 | ||
92 | async function proxifyPasswordGrant (req: express.Request, res: express.Response) { | |
93 | const plugins = PluginManager.Instance.getIdAndPassAuths() | |
94 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | |
95 | ||
96 | for (const plugin of plugins) { | |
97 | const auths = plugin.idAndPassAuths | |
98 | ||
99 | for (const auth of auths) { | |
100 | pluginAuths.push({ | |
101 | npmName: plugin.npmName, | |
102 | registerAuthOptions: auth | |
103 | }) | |
104 | } | |
105 | } | |
106 | ||
107 | pluginAuths.sort((a, b) => { | |
108 | const aWeight = a.registerAuthOptions.getWeight() | |
109 | const bWeight = b.registerAuthOptions.getWeight() | |
110 | ||
111 | // DESC weight order | |
112 | if (aWeight === bWeight) return 0 | |
113 | if (aWeight < bWeight) return 1 | |
114 | return -1 | |
115 | }) | |
116 | ||
117 | const loginOptions = { | |
118 | id: req.body.username, | |
119 | password: req.body.password | |
120 | } | |
121 | ||
122 | for (const pluginAuth of pluginAuths) { | |
123 | const authOptions = pluginAuth.registerAuthOptions | |
124 | const authName = authOptions.authName | |
125 | const npmName = pluginAuth.npmName | |
126 | ||
127 | logger.debug( | |
128 | 'Using auth method %s of plugin %s to login %s with weight %d.', | |
129 | authName, npmName, loginOptions.id, authOptions.getWeight() | |
130 | ) | |
131 | ||
132 | try { | |
133 | const loginResult = await authOptions.login(loginOptions) | |
134 | if (loginResult) { | |
135 | logger.info( | |
136 | 'Login success with auth method %s of plugin %s for %s.', | |
137 | authName, npmName, loginOptions.id | |
138 | ) | |
139 | ||
140 | if (!isUserUsernameValid(loginResult.username)) { | |
141 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { loginResult }) | |
142 | continue | |
143 | } | |
144 | ||
145 | if (!loginResult.email) { | |
146 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult }) | |
147 | continue | |
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 | } | |
176 | } catch (err) { | |
177 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | |
178 | } | |
179 | } | |
180 | } |