Skip to content

Commit 3281854

Browse files
alex-fusionauthcodercatdevfalcowinkler
authored
adjust default fusionauth provider details (#10868)
--------- Co-authored-by: Alex Patterson <[email protected]> Co-authored-by: Falco Winkler <[email protected]>
1 parent 9886bd4 commit 3281854

File tree

1 file changed

+205
-5
lines changed

1 file changed

+205
-5
lines changed

packages/core/src/providers/fusionauth.ts

Lines changed: 205 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
108277
export 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

Comments
 (0)