-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathlink-authorize-url.ts
More file actions
123 lines (112 loc) · 4.34 KB
/
link-authorize-url.ts
File metadata and controls
123 lines (112 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { randomUUID } from 'node:crypto'
import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import { Type } from '@sinclair/typebox'
import { and, eq, gte, sql } from 'drizzle-orm'
import type { FastifyPluginAsync } from 'fastify'
import { getDb } from '../../../../db/index.js'
import { verification } from '../../../../db/schema/index.js'
import { env } from '../../../../lib/env.js'
import { hashToken } from '../../../../lib/jwt.js'
import {
getOAuthAllowedCallbackUrls,
resolveOAuthCallbackUrl,
} from '../../../../lib/oauth-shared.js'
import { ErrorResponseSchema } from '../../../schemas.js'
const linkAuthorizeUrlPerUserPerHour = 10
const AuthorizeUrlResponseSchema = Type.Object({
redirectUrl: Type.String(),
})
const LinkAuthorizeUrlQuerystringSchema = Type.Object({
redirect_uri: Type.Optional(Type.String()),
})
const oauthLinkAuthorizeUrlRoute: FastifyPluginAsync = async fastify => {
fastify.withTypeProvider<TypeBoxTypeProvider>().get(
'/link-authorize-url',
{
schema: {
operationId: 'oauthGithubLinkAuthorizeUrl',
description: 'Return GitHub OAuth URL for linking account (Bearer required)',
summary: 'GitHub OAuth link authorize URL',
tags: ['auth'],
security: [{ bearerAuth: [] }],
querystring: LinkAuthorizeUrlQuerystringSchema,
response: {
200: AuthorizeUrlResponseSchema,
401: ErrorResponseSchema,
400: ErrorResponseSchema,
429: ErrorResponseSchema,
503: ErrorResponseSchema,
},
},
},
async (request, reply) => {
if (!request.session)
return reply.code(401).send({
code: 'UNAUTHORIZED',
message: 'Authentication required',
})
const githubClientId = env.GITHUB_CLIENT_ID
const githubClientSecret = env.GITHUB_CLIENT_SECRET
const allowedUrls = getOAuthAllowedCallbackUrls({
urls: env.OAUTH_GITHUB_CALLBACK_URLS,
singleUrl: env.OAUTH_GITHUB_CALLBACK_URL,
})
const resolved = resolveOAuthCallbackUrl({
allowedUrls,
requestedRedirectUri: request.query.redirect_uri,
})
if (!resolved.ok)
return reply.status(resolved.error === 'NOT_CONFIGURED' ? 503 : 400).send({
code:
resolved.error === 'NOT_CONFIGURED' ? 'OAUTH_NOT_CONFIGURED' : 'INVALID_REDIRECT_URI',
message:
resolved.error === 'NOT_CONFIGURED'
? 'GitHub OAuth is not configured'
: 'redirect_uri must be one of the configured callback URLs',
})
if (!githubClientId || !githubClientSecret)
return reply.status(503).send({
code: 'OAUTH_NOT_CONFIGURED',
message: 'GitHub OAuth is not configured',
})
const { redirectUri } = resolved
const userId = request.session.user.id
const db = await getDb()
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000)
const [recentCount] = await db
.select({ count: sql<number>`count(*)::int` })
.from(verification)
.where(
and(
eq(verification.type, 'oauth_link_state'),
eq(verification.identifier, `link:${userId}`),
gte(verification.createdAt, oneHourAgo),
),
)
if ((recentCount?.count ?? 0) >= linkAuthorizeUrlPerUserPerHour)
return reply.code(429).send({
code: 'TOO_MANY_REQUESTS',
message: 'Too many link requests. Try again later.',
})
const state = randomUUID() + randomUUID().replace(/-/g, '')
const stateHash = hashToken(state)
const expiresAt = new Date(Date.now() + 10 * 60 * 1000)
await db.insert(verification).values({
id: randomUUID(),
type: 'oauth_link_state',
identifier: `link:${userId}`,
value: stateHash,
meta: { userId, redirectUri },
expiresAt,
})
const redirectUrl = new URL('https://github.com/login/oauth/authorize')
redirectUrl.searchParams.set('client_id', githubClientId)
redirectUrl.searchParams.set('redirect_uri', redirectUri)
redirectUrl.searchParams.set('scope', 'user:email')
redirectUrl.searchParams.set('state', state)
return reply.status(200).send({ redirectUrl: redirectUrl.toString() })
},
)
}
export default oauthLinkAuthorizeUrlRoute
export const prefixOverride = '/auth/oauth/github'