-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathoauth-google.ts
More file actions
95 lines (91 loc) · 3.21 KB
/
oauth-google.ts
File metadata and controls
95 lines (91 loc) · 3.21 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
const fetchTimeoutMs = 15_000
export type GoogleTokenResponse = {
/* biome-ignore lint/style/useNamingConvention: OAuth API uses snake_case */
access_token: string
/* biome-ignore lint/style/useNamingConvention: OAuth API uses snake_case */
token_type: string
/* biome-ignore lint/style/useNamingConvention: OAuth API uses snake_case */
expires_in?: number
scope?: string
/* biome-ignore lint/style/useNamingConvention: OAuth API uses snake_case */
refresh_token?: string
/* biome-ignore lint/style/useNamingConvention: OAuth API uses snake_case */
id_token?: string
error?: string
}
export type GoogleUser = {
id: string
email?: string
name?: string
/* biome-ignore lint/style/useNamingConvention: OAuth API uses snake_case */
verified_email?: boolean
}
export async function fetchGoogleTokens(input: {
code: string
codeVerifier: string
redirectUri: string
clientId: string
clientSecret: string
}): Promise<GoogleTokenResponse> {
const { code, codeVerifier, redirectUri, clientId, clientSecret } = input
const tokenBody = new URLSearchParams({
code,
// biome-ignore lint/style/useNamingConvention: OAuth spec uses snake_case
client_id: clientId,
// biome-ignore lint/style/useNamingConvention: OAuth spec uses snake_case
client_secret: clientSecret,
// biome-ignore lint/style/useNamingConvention: OAuth spec uses snake_case
redirect_uri: redirectUri,
// biome-ignore lint/style/useNamingConvention: OAuth spec uses snake_case
grant_type: 'authorization_code',
// biome-ignore lint/style/useNamingConvention: OAuth spec uses snake_case
code_verifier: codeVerifier,
})
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenBody.toString(),
signal: AbortSignal.timeout(fetchTimeoutMs),
})
const tokenData = (await tokenRes.json()) as GoogleTokenResponse
if (!tokenRes.ok) {
const err = new Error('Token exchange failed') as Error & {
status: number
tokenData: GoogleTokenResponse
}
err.status = tokenRes.status
err.tokenData = tokenData
throw err
}
if (tokenData.error || !tokenData.access_token) {
const err = new Error('Token exchange failed') as Error & {
status: number
tokenData: GoogleTokenResponse
}
err.status = 400
err.tokenData = tokenData
throw err
}
return tokenData
}
export async function fetchGoogleUserInfo(accessToken: string): Promise<GoogleUser> {
const userRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
// biome-ignore lint/style/useNamingConvention: HTTP header canonical form
headers: { Authorization: `Bearer ${accessToken}` },
signal: AbortSignal.timeout(fetchTimeoutMs),
})
const gUser = (await userRes.json()) as GoogleUser
if (!userRes.ok) {
const err = new Error('User info fetch failed') as Error & { status: number; gUser: GoogleUser }
err.status = userRes.status
err.gUser = gUser
throw err
}
if (!gUser?.id) {
const err = new Error('Invalid user response') as Error & { status: number; gUser: GoogleUser }
err.status = 400
err.gUser = gUser
throw err
}
return gUser
}