@@ -24,11 +24,16 @@ export interface FusionAuthProfile extends Record<string, any> {
2424 authenticationType : string
2525 email : string
2626 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
2832 at_hash : string
2933 c_hash : string
3034 scope : string
3135 sid : string
36+ picture ?: string
3237}
3338
3439/**
@@ -64,7 +69,7 @@ export interface FusionAuthProfile extends Record<string, any> {
6469 *
6570 * ### Resources
6671 *
67- * - [FusionAuth OAuth documentation](https://fusionauth.io/docs/v1/tech /oauth/)
72+ * - [FusionAuth OAuth documentation](https://fusionauth.io/docs/lifecycle/authenticate-users /oauth/)
6873 *
6974 * ### Notes
7075 *
@@ -104,6 +109,170 @@ export interface FusionAuthProfile extends Record<string, any> {
104109 * we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
105110 *
106111 * :::
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+ *
107276 */
108277export default function FusionAuth < P extends FusionAuthProfile > (
109278 // 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>(
112281 return {
113282 id : "fusionauth" ,
114283 name : "FusionAuth" ,
115- type : "oauth" ,
284+ type : "oidc" ,
285+ issuer : options . issuer ,
286+ clientId : options . clientId ,
287+ clientSecret : options . clientSecret ,
116288 wellKnown : options ?. tenantId
117289 ? `${ options . issuer } /.well-known/openid-configuration?tenantId=${ options . tenantId } `
118290 : `${ options . issuer } /.well-known/openid-configuration` ,
119291 authorization : {
120292 params : {
121- scope : "openid offline_access" ,
293+ scope : "openid offline_access email profile " ,
122294 ...( options ?. tenantId && { tenantId : options . tenantId } ) ,
123295 } ,
124296 } ,
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+ } ,
125319 checks : [ "pkce" , "state" ] ,
126320 profile ( profile ) {
127321 return {
128322 id : profile . sub ,
129323 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 ,
131331 }
132332 } ,
133333 options,
0 commit comments