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