]>
Commit | Line | Data |
---|---|---|
1 | import express from 'express' | |
2 | import OAuth2Server, { | |
3 | InvalidClientError, | |
4 | InvalidGrantError, | |
5 | InvalidRequestError, | |
6 | Request, | |
7 | Response, | |
8 | UnauthorizedClientError, | |
9 | UnsupportedGrantTypeError | |
10 | } from '@node-oauth/oauth2-server' | |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | |
12 | import { isOTPValid } from '@server/helpers/otp' | |
13 | import { CONFIG } from '@server/initializers/config' | |
14 | import { UserRegistrationModel } from '@server/models/user/user-registration' | |
15 | import { MOAuthClient } from '@server/types/models' | |
16 | import { sha1 } from '@shared/extra-utils' | |
17 | import { HttpStatusCode, ServerErrorCode, UserRegistrationState } from '@shared/models' | |
18 | import { OTP } from '../../initializers/constants' | |
19 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | |
20 | ||
21 | class MissingTwoFactorError extends Error { | |
22 | code = HttpStatusCode.UNAUTHORIZED_401 | |
23 | name = ServerErrorCode.MISSING_TWO_FACTOR | |
24 | } | |
25 | ||
26 | class InvalidTwoFactorError extends Error { | |
27 | code = HttpStatusCode.BAD_REQUEST_400 | |
28 | name = ServerErrorCode.INVALID_TWO_FACTOR | |
29 | } | |
30 | ||
31 | class RegistrationWaitingForApproval extends Error { | |
32 | code = HttpStatusCode.BAD_REQUEST_400 | |
33 | name = ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL | |
34 | } | |
35 | ||
36 | class RegistrationApprovalRejected extends Error { | |
37 | code = HttpStatusCode.BAD_REQUEST_400 | |
38 | name = ServerErrorCode.ACCOUNT_APPROVAL_REJECTED | |
39 | } | |
40 | ||
41 | /** | |
42 | * | |
43 | * Reimplement some functions of OAuth2Server to inject external auth methods | |
44 | * | |
45 | */ | |
46 | const oAuthServer = new OAuth2Server({ | |
47 | // Wants seconds | |
48 | accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, | |
49 | refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, | |
50 | ||
51 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | |
52 | model: require('./oauth-model') | |
53 | }) | |
54 | ||
55 | // --------------------------------------------------------------------------- | |
56 | ||
57 | async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) { | |
58 | const request = new Request(req) | |
59 | const { refreshTokenAuthName, bypassLogin } = options | |
60 | ||
61 | if (request.method !== 'POST') { | |
62 | throw new InvalidRequestError('Invalid request: method must be POST') | |
63 | } | |
64 | ||
65 | if (!request.is([ 'application/x-www-form-urlencoded' ])) { | |
66 | throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded') | |
67 | } | |
68 | ||
69 | const clientId = request.body.client_id | |
70 | const clientSecret = request.body.client_secret | |
71 | ||
72 | if (!clientId || !clientSecret) { | |
73 | throw new InvalidClientError('Invalid client: cannot retrieve client credentials') | |
74 | } | |
75 | ||
76 | const client = await getClient(clientId, clientSecret) | |
77 | if (!client) { | |
78 | throw new InvalidClientError('Invalid client: client is invalid') | |
79 | } | |
80 | ||
81 | const grantType = request.body.grant_type | |
82 | if (!grantType) { | |
83 | throw new InvalidRequestError('Missing parameter: `grant_type`') | |
84 | } | |
85 | ||
86 | if (![ 'password', 'refresh_token' ].includes(grantType)) { | |
87 | throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid') | |
88 | } | |
89 | ||
90 | if (!client.grants.includes(grantType)) { | |
91 | throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid') | |
92 | } | |
93 | ||
94 | if (grantType === 'password') { | |
95 | return handlePasswordGrant({ | |
96 | request, | |
97 | client, | |
98 | bypassLogin | |
99 | }) | |
100 | } | |
101 | ||
102 | return handleRefreshGrant({ | |
103 | request, | |
104 | client, | |
105 | refreshTokenAuthName | |
106 | }) | |
107 | } | |
108 | ||
109 | function handleOAuthAuthenticate ( | |
110 | req: express.Request, | |
111 | res: express.Response | |
112 | ) { | |
113 | return oAuthServer.authenticate(new Request(req), new Response(res)) | |
114 | } | |
115 | ||
116 | export { | |
117 | MissingTwoFactorError, | |
118 | InvalidTwoFactorError, | |
119 | ||
120 | handleOAuthToken, | |
121 | handleOAuthAuthenticate | |
122 | } | |
123 | ||
124 | // --------------------------------------------------------------------------- | |
125 | ||
126 | async function handlePasswordGrant (options: { | |
127 | request: Request | |
128 | client: MOAuthClient | |
129 | bypassLogin?: BypassLogin | |
130 | }) { | |
131 | const { request, client, bypassLogin } = options | |
132 | ||
133 | if (!request.body.username) { | |
134 | throw new InvalidRequestError('Missing parameter: `username`') | |
135 | } | |
136 | ||
137 | if (!bypassLogin && !request.body.password) { | |
138 | throw new InvalidRequestError('Missing parameter: `password`') | |
139 | } | |
140 | ||
141 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | |
142 | if (!user) { | |
143 | const registration = await UserRegistrationModel.loadByEmailOrUsername(request.body.username) | |
144 | ||
145 | if (registration?.state === UserRegistrationState.REJECTED) { | |
146 | throw new RegistrationApprovalRejected('Registration approval for this account has been rejected') | |
147 | } else if (registration?.state === UserRegistrationState.PENDING) { | |
148 | throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval') | |
149 | } | |
150 | ||
151 | throw new InvalidGrantError('Invalid grant: user credentials are invalid') | |
152 | } | |
153 | ||
154 | if (user.otpSecret) { | |
155 | if (!request.headers[OTP.HEADER_NAME]) { | |
156 | throw new MissingTwoFactorError('Missing two factor header') | |
157 | } | |
158 | ||
159 | if (await isOTPValid({ encryptedSecret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) { | |
160 | throw new InvalidTwoFactorError('Invalid two factor header') | |
161 | } | |
162 | } | |
163 | ||
164 | const token = await buildToken() | |
165 | ||
166 | return saveToken(token, client, user, { bypassLogin }) | |
167 | } | |
168 | ||
169 | async function handleRefreshGrant (options: { | |
170 | request: Request | |
171 | client: MOAuthClient | |
172 | refreshTokenAuthName: string | |
173 | }) { | |
174 | const { request, client, refreshTokenAuthName } = options | |
175 | ||
176 | if (!request.body.refresh_token) { | |
177 | throw new InvalidRequestError('Missing parameter: `refresh_token`') | |
178 | } | |
179 | ||
180 | const refreshToken = await getRefreshToken(request.body.refresh_token) | |
181 | ||
182 | if (!refreshToken) { | |
183 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | |
184 | } | |
185 | ||
186 | if (refreshToken.client.id !== client.id) { | |
187 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | |
188 | } | |
189 | ||
190 | if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) { | |
191 | throw new InvalidGrantError('Invalid grant: refresh token has expired') | |
192 | } | |
193 | ||
194 | await revokeToken({ refreshToken: refreshToken.refreshToken }) | |
195 | ||
196 | const token = await buildToken() | |
197 | ||
198 | return saveToken(token, client, refreshToken.user, { refreshTokenAuthName }) | |
199 | } | |
200 | ||
201 | function generateRandomToken () { | |
202 | return randomBytesPromise(256) | |
203 | .then(buffer => sha1(buffer)) | |
204 | } | |
205 | ||
206 | function getTokenExpiresAt (type: 'access' | 'refresh') { | |
207 | const lifetime = type === 'access' | |
208 | ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN | |
209 | : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN | |
210 | ||
211 | return new Date(Date.now() + lifetime) | |
212 | } | |
213 | ||
214 | async function buildToken () { | |
215 | const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ]) | |
216 | ||
217 | return { | |
218 | accessToken, | |
219 | refreshToken, | |
220 | accessTokenExpiresAt: getTokenExpiresAt('access'), | |
221 | refreshTokenExpiresAt: getTokenExpiresAt('refresh') | |
222 | } | |
223 | } |