@@ -24,11 +24,16 @@ export interface FusionAuthProfile extends Record<string, any> {
24
24
authenticationType : string
25
25
email : string
26
26
email_verified : boolean
27
- preferred_username : string
27
+ preferred_username ?: string
28
+ name ?: string
29
+ given_name ?: string
30
+ middle_name ?: string
31
+ family_name ?: string
28
32
at_hash : string
29
33
c_hash : string
30
34
scope : string
31
35
sid : string
36
+ picture ?: string
32
37
}
33
38
34
39
/**
@@ -64,7 +69,7 @@ export interface FusionAuthProfile extends Record<string, any> {
64
69
*
65
70
* ### Resources
66
71
*
67
- * - [FusionAuth OAuth documentation](https://fusionauth.io/docs/v1/tech /oauth/)
72
+ * - [FusionAuth OAuth documentation](https://fusionauth.io/docs/lifecycle/authenticate-users /oauth/)
68
73
*
69
74
* ### Notes
70
75
*
@@ -104,6 +109,170 @@ export interface FusionAuthProfile extends Record<string, any> {
104
109
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
105
110
*
106
111
* :::
112
+ *
113
+ *
114
+ * It is highly recommended to follow this example call when using the provider in Next.js
115
+ * so that you can access both the access_token and id_token on the server.
116
+ *
117
+ * /// <reference types="next-auth" />
118
+ import NextAuth from 'next-auth';
119
+ export const { handlers, auth, signIn, signOut } = NextAuth({
120
+ providers: [
121
+ {
122
+ id: 'fusionauth',
123
+ name: 'FusionAuth',
124
+ type: 'oidc',
125
+ issuer: process.env.AUTH_FUSIONAUTH_ISSUER!,
126
+ clientId: process.env.AUTH_FUSIONAUTH_CLIENT_ID!,
127
+ clientSecret: process.env.AUTH_FUSIONAUTH_CLIENT_SECRET!,
128
+ authorization: {
129
+ params: {
130
+ scope: 'offline_access email openid profile',
131
+ tenantId: process.env.AUTH_FUSIONAUTH_TENANT_ID!,
132
+ },
133
+ },
134
+ userinfo: `${process.env.AUTH_FUSIONAUTH_ISSUER}/oauth2/userinfo`,
135
+ // This is due to a known processing issue
136
+ // TODO: https://github.com/nextauthjs/next-auth/issues/8745#issuecomment-1907799026
137
+ token: {
138
+ url: `${process.env.AUTH_FUSIONAUTH_ISSUER}/oauth2/token`,
139
+ conform: async (response: Response) => {
140
+ if (response.status === 401) return response;
141
+
142
+ const newHeaders = Array.from(response.headers.entries())
143
+ .filter(([key]) => key.toLowerCase() !== 'www-authenticate')
144
+ .reduce(
145
+ (headers, [key, value]) => (headers.append(key, value), headers),
146
+ new Headers()
147
+ );
148
+
149
+ return new Response(response.body, {
150
+ status: response.status,
151
+ statusText: response.statusText,
152
+ headers: newHeaders,
153
+ });
154
+ },
155
+ },
156
+ },
157
+ ],
158
+ session: {
159
+ strategy: 'jwt',
160
+ },
161
+ // Required to get the account object in the session and enable
162
+ // the ability to call API's externally that rely on JWT tokens.
163
+ callbacks: {
164
+ async jwt(params) {
165
+ const { token, user, account } = params;
166
+ if (account) {
167
+ // First-time login, save the `access_token`, its expiry and the `refresh_token`
168
+ return {
169
+ ...token,
170
+ ...account,
171
+ };
172
+ } else if (
173
+ token.expires_at &&
174
+ Date.now() < (token.expires_at as number) * 1000
175
+ ) {
176
+ // Subsequent logins, but the `access_token` is still valid
177
+ return token;
178
+ } else {
179
+ // Subsequent logins, but the `access_token` has expired, try to refresh it
180
+ if (!token.refresh_token) throw new TypeError('Missing refresh_token');
181
+
182
+ try {
183
+ const refreshResponse = await fetch(
184
+ `${process.env.AUTH_FUSIONAUTH_ISSUER}/oauth2/token`,
185
+ {
186
+ method: 'POST',
187
+ headers: {
188
+ 'Content-Type': 'application/x-www-form-urlencoded',
189
+ },
190
+ body: new URLSearchParams({
191
+ client_id: process.env.AUTH_FUSIONAUTH_CLIENT_ID!,
192
+ client_secret: process.env.AUTH_FUSIONAUTH_CLIENT_SECRET!,
193
+ grant_type: 'refresh_token',
194
+ refresh_token: token.refresh_token as string,
195
+ }),
196
+ }
197
+ );
198
+
199
+ if (!refreshResponse.ok) {
200
+ throw new Error('Failed to refresh token');
201
+ }
202
+
203
+ const tokensOrError = await refreshResponse.json();
204
+
205
+ if (!refreshResponse.ok) throw tokensOrError;
206
+
207
+ const newTokens = tokensOrError as {
208
+ access_token: string;
209
+ expires_in: number;
210
+ refresh_token?: string;
211
+ };
212
+
213
+ return {
214
+ ...token,
215
+ access_token: newTokens.access_token,
216
+ expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
217
+ // Some providers only issue refresh tokens once, so preserve if we did not get a new one
218
+ refresh_token: newTokens.refresh_token
219
+ ? newTokens.refresh_token
220
+ : token.refresh_token,
221
+ };
222
+ } catch (error) {
223
+ console.error('Error refreshing access_token', error);
224
+ // If we fail to refresh the token, return an error so we can handle it on the page
225
+ token.error = 'RefreshTokenError';
226
+ return token;
227
+ }
228
+ }
229
+ },
230
+ async session(params) {
231
+ const { session, token } = params;
232
+ return { ...session, ...token };
233
+ },
234
+ },
235
+ });
236
+
237
+ declare module 'next-auth' {
238
+ interface Session {
239
+ access_token: string;
240
+ expires_in: number;
241
+ id_token?: string;
242
+ expires_at: number;
243
+ refresh_token?: string;
244
+ refresh_token_id?: string;
245
+ error?: 'RefreshTokenError';
246
+ scope: string;
247
+ token_type: string;
248
+ userId: string;
249
+ provider: string;
250
+ type: string;
251
+ providerAccountId: string;
252
+ }
253
+ }
254
+
255
+ declare module 'next-auth' {
256
+ interface JWT {
257
+ access_token: string;
258
+ expires_in: number;
259
+ id_token?: string;
260
+ expires_at: number;
261
+ refresh_token?: string;
262
+ refresh_token_id?: string;
263
+ error?: 'RefreshTokenError';
264
+ scope: string;
265
+ token_type: string;
266
+ userId: string;
267
+ provider: string;
268
+ type: string;
269
+ providerAccountId: string;
270
+ }
271
+ }
272
+
273
+ *
274
+ *
275
+ *
107
276
*/
108
277
export default function FusionAuth < P extends FusionAuthProfile > (
109
278
// tenantId only needed if there is more than one tenant configured on the server
@@ -112,22 +281,53 @@ export default function FusionAuth<P extends FusionAuthProfile>(
112
281
return {
113
282
id : "fusionauth" ,
114
283
name : "FusionAuth" ,
115
- type : "oauth" ,
284
+ type : "oidc" ,
285
+ issuer : options . issuer ,
286
+ clientId : options . clientId ,
287
+ clientSecret : options . clientSecret ,
116
288
wellKnown : options ?. tenantId
117
289
? `${ options . issuer } /.well-known/openid-configuration?tenantId=${ options . tenantId } `
118
290
: `${ options . issuer } /.well-known/openid-configuration` ,
119
291
authorization : {
120
292
params : {
121
- scope : "openid offline_access" ,
293
+ scope : "openid offline_access email profile " ,
122
294
...( options ?. tenantId && { tenantId : options . tenantId } ) ,
123
295
} ,
124
296
} ,
297
+ userinfo : `${ options . issuer } /oauth2/userinfo` ,
298
+ // This is due to a known processing issue
299
+ // TODO: https://github.com/nextauthjs/next-auth/issues/8745#issuecomment-1907799026
300
+ token : {
301
+ url : `${ options . issuer } /oauth2/token` ,
302
+ conform : async ( response : Response ) => {
303
+ if ( response . status === 401 ) return response
304
+
305
+ const newHeaders = Array . from ( response . headers . entries ( ) )
306
+ . filter ( ( [ key ] ) => key . toLowerCase ( ) !== "www-authenticate" )
307
+ . reduce (
308
+ ( headers , [ key , value ] ) => ( headers . append ( key , value ) , headers ) ,
309
+ new Headers ( )
310
+ )
311
+
312
+ return new Response ( response . body , {
313
+ status : response . status ,
314
+ statusText : response . statusText ,
315
+ headers : newHeaders ,
316
+ } )
317
+ } ,
318
+ } ,
125
319
checks : [ "pkce" , "state" ] ,
126
320
profile ( profile ) {
127
321
return {
128
322
id : profile . sub ,
129
323
email : profile . email ,
130
- name : profile ?. preferred_username ,
324
+ name :
325
+ profile . name ??
326
+ profile . preferred_username ??
327
+ [ profile . given_name , profile . middle_name , profile . family_name ]
328
+ . filter ( ( x ) => x )
329
+ . join ( " " ) ,
330
+ image : profile . picture ,
131
331
}
132
332
} ,
133
333
options,
0 commit comments