]>
Commit | Line | Data |
---|---|---|
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 | } |