aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/auth/oauth.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-03-12 15:20:46 +0100
committerChocobozzz <me@florianbigard.com>2021-03-24 18:18:41 +0100
commitf43db2f46ee50bacb402a6ef42d768694c2bc9a8 (patch)
treebce2574e94d48e8602387615a07ee691e98e23e4 /server/lib/auth/oauth.ts
parentcae2df6bdc3c3590df32bf7431a617177be30429 (diff)
downloadPeerTube-f43db2f46ee50bacb402a6ef42d768694c2bc9a8.tar.gz
PeerTube-f43db2f46ee50bacb402a6ef42d768694c2bc9a8.tar.zst
PeerTube-f43db2f46ee50bacb402a6ef42d768694c2bc9a8.zip
Refactor auth flow
Reimplement some node-oauth2-server methods to remove hacky code needed by our external login workflow
Diffstat (limited to 'server/lib/auth/oauth.ts')
-rw-r--r--server/lib/auth/oauth.ts180
1 files changed, 180 insertions, 0 deletions
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
new file mode 100644
index 000000000..5b6130d56
--- /dev/null
+++ b/server/lib/auth/oauth.ts
@@ -0,0 +1,180 @@
1import * as express from 'express'
2import {
3 InvalidClientError,
4 InvalidGrantError,
5 InvalidRequestError,
6 Request,
7 Response,
8 UnauthorizedClientError,
9 UnsupportedGrantTypeError
10} from 'oauth2-server'
11import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
12import { MOAuthClient } from '@server/types/models'
13import { OAUTH_LIFETIME } from '../../initializers/constants'
14import { 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
22const 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
32async 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
84async 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
96export {
97 handleOAuthToken,
98 handleOAuthAuthenticate
99}
100
101// ---------------------------------------------------------------------------
102
103async 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
126async 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
158function generateRandomToken () {
159 return randomBytesPromise(256)
160 .then(buffer => sha1(buffer))
161}
162
163function 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
171async 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}