-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
/
callback.js
169 lines (152 loc) · 5.58 KB
/
callback.js
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { createHash } from 'crypto'
import { decode as jwtDecode } from 'jsonwebtoken'
import oAuthClient from './client'
import logger from '../../../lib/logger'
class OAuthCallbackError extends Error {
constructor (message) {
super(message)
this.name = 'OAuthCallbackError'
this.message = message
}
}
export default async function oAuthCallback (req) {
const { provider, csrfToken, pkce } = req.options
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
// The "user" object is specific to the Apple provider and is provided on first sign in
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
let { code, user, state } = req.query // eslint-disable-line camelcase
// For OAuth 2.0 flows, check state returned and matches expected value
// (a hash of the NextAuth.js CSRF token).
//
// Apple does not support state verification.
if (provider.id !== 'apple') {
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
if (state !== expectedState) {
throw new OAuthCallbackError('Invalid state returned from OAuth provider')
}
}
if (req.method === 'POST') {
try {
const body = JSON.parse(JSON.stringify(req.body))
if (body.error) {
throw new Error(body.error)
}
code = body.code
user = body.user != null ? JSON.parse(body.user) : null
} catch (error) {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
throw error
}
}
// REVIEW: Is this used by any of the providers?
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
} else {
client.useAuthorizationHeaderforGET(true)
}
try {
const { accessToken, refreshToken, results } = await client.getOAuthAccessToken(code, provider, pkce.code_verifier)
const tokens = { accessToken, refreshToken, idToken: results.id_token }
let profileData
if (provider.idToken) {
// If we don't have an ID Token most likely the user hit a cancel
// button when signing in (or the provider is misconfigured).
//
// Unfortunately, we can't tell which, so we can't treat it as an
// error, so instead we just returning nothing, which will cause the
// user to be redirected back to the sign in page.
if (!results?.id_token) {
throw new OAuthCallbackError()
}
// Support services that use OpenID ID Tokens to encode profile data
profileData = decodeIdToken(results.id_token)
} else {
profileData = await client.get(provider, accessToken, results)
}
return _getProfile({ profileData, provider, tokens, user })
} catch (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
throw error
}
}
try {
// Handle OAuth v1.x
const {
oauth_token: oauthToken, oauth_verifier: oauthVerifier
} = req.query
const { accessToken, refreshToken, results } = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
const profileData = await client.get(
provider.profileUrl,
accessToken,
refreshToken
)
const tokens = {
accessToken, refreshToken, idToken: results.id_token
}
return _getProfile({
profileData, tokens, provider
})
} catch (error) {
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
throw error
}
}
/**
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
* Returns profile, raw profile and auth provider details
*/
async function _getProfile ({
profileData, tokens: { accessToken, refreshToken, idToken }, provider, user
}) {
try {
// Convert profileData into an object if it's a string
if (typeof profileData === 'string' || profileData instanceof String) {
profileData = JSON.parse(profileData)
}
// If a user object is supplied (e.g. Apple provider) add it to the profile object
if (user != null) {
profileData.user = user
}
profileData.idToken = idToken
logger.debug('PROFILE_DATA', profileData)
const profile = await provider.profile(profileData)
// Return profile, raw profile and auth provider details
return {
profile: {
...profile,
email: profile.email?.toLowerCase() ?? null
},
account: {
provider: provider.id,
type: provider.type,
id: profile.id,
refreshToken,
accessToken,
accessTokenExpires: null
},
OAuthProfile: profileData
}
} catch (exception) {
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortuately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData)
return {
profile: null,
account: null,
OAuthProfile: profileData
}
}
}
function decodeIdToken (idToken) {
if (!idToken) {
throw new OAuthCallbackError('Missing JWT ID Token')
}
return jwtDecode(idToken, { json: true })
}