diff --git a/package-lock.json b/package-lock.json index 7d1a0148278e4085556d1e1f4cfedd421ad4ec6b..bdb592671fb8394c21092da8a6a6bb41c5820ff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@types/cytoscape-fcose": "^2.2.4", "@types/leaflet": "^1.9.12", "@types/node": "^20.10.5", + "@types/node-fetch": "^2.6.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.2.18", "@types/react-leaflet-markercluster": "^3.0.4", @@ -3291,6 +3292,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/numeral": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.5.tgz", diff --git a/package.json b/package.json index 80f1b02c8dd829ca26f89cc07a8dbcf4140a81eb..a3349c236e8a15032c6227e9b30663b18f5ba7b9 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/cytoscape-fcose": "^2.2.4", "@types/leaflet": "^1.9.12", "@types/node": "^20.10.5", + "@types/node-fetch": "^2.6.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.2.18", "@types/react-leaflet-markercluster": "^3.0.4", diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 6bc263531e475708b23775a74eba2e876c15e586..2a5307254390aa40525302ef0a9fa92cbeb5933a 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -5,25 +5,69 @@ import { import NextAuth, { AuthOptions } from 'next-auth'; import { JWT } from 'next-auth/jwt'; import { OAuthConfig } from 'next-auth/providers'; +import fetch from 'node-fetch'; const token_endpoint_auth_method = process.env.NEXTAUTH_CLIENT_SECRET ? 'client_secret_basic' : 'none'; -const authActive = process.env.AUTH_ACTIVE?.toLowerCase() != 'false'; +const authActive = process.env.AUTH_ACTIVE?.toLowerCase() !== 'false'; + +async function refreshAccessToken(token: JWT): Promise<JWT> { + try { + const raw = JSON.stringify({ + client_id: process.env.NEXTAUTH_CLIENT_ID, + grant_type: 'refresh_token', + refresh_token: token.refreshToken as string, + }); + + const response = await fetch(process.env.TOKEN_ENDPOINT!, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: raw, + redirect: 'follow', + }); + + if (response.ok) { + const data: { + access_token: string; + expires_in: number; + refresh_token: string; + } = await response.json(); + + return { + ...token, + accessToken: data.access_token, + accessTokenExpires: + Math.floor(Date.now() / 1000) + (data.expires_in - 300), // Store expiry as a Unix timestamp (in seconds) + refreshToken: data.refresh_token, + }; + } else { + return { + ...token, + error: `${response.statusText}`, + }; + } + } catch (error) { + return { + ...token, + error: `${error}`, + }; + } +} + const wfoProvider: OAuthConfig<WfoUserProfile> = { id: process.env.NEXTAUTH_ID || '', - name: process.env.NEXTAUTH_ID || '', + name: 'GEANT Identity Provider', type: 'oauth', - clientId: process.env.NEXTAUTH_CLIENT_ID || '', - clientSecret: process.env.NEXTAUTH_CLIENT_SECRET || undefined, - wellKnown: - process.env.NEXTAUTH_WELL_KNOWN_OVERRIDE ?? - `${process.env.NEXTAUTH_ISSUER || ''}/.well-known/openid-configuration`, + clientId: process.env.NEXTAUTH_CLIENT_ID, + wellKnown: process.env.NEXTAUTH_WELL_KNOWN_OVERRIDE, authorization: { params: { - scope: - process.env.NEXTAUTH_AUTHORIZATION_SCOPE_OVERRIDE ?? 'openid profile', + scope: process.env.NEXTAUTH_AUTHORIZATION_SCOPE_OVERRIDE, + prompt: 'consent', }, }, idToken: true, @@ -35,7 +79,6 @@ const wfoProvider: OAuthConfig<WfoUserProfile> = { if (!context.provider.wellKnown || !tokens.access_token) { return {}; } - return await client.userinfo(tokens.access_token); }, }, @@ -55,13 +98,25 @@ const wfoProvider: OAuthConfig<WfoUserProfile> = { export const authOptions: AuthOptions = { providers: authActive ? [wfoProvider] : [], callbacks: { - async jwt({ token, account, profile }) { + async jwt({ token, account, profile }): Promise<JWT> { // The "account" is only available right after signing in -- adding useful data to the token if (account) { token.accessToken = account.access_token; - token.profile = profile; + token.refreshToken = account.refresh_token; + token.accessTokenExpires = account.expires_at as number; + + token.profile = profile as WfoUserProfile; + } + + const now = Math.floor(Date.now() / 1000); + if ( + typeof token.accessTokenExpires === 'number' && + now < token.accessTokenExpires + ) { + return token; } - return token; + + return await refreshAccessToken(token); }, async session({ session, token }: { session: WfoSession; token: JWT }) { // Assign data to the session to be available in the client through the useSession hook @@ -72,4 +127,5 @@ export const authOptions: AuthOptions = { }, }, }; + export default NextAuth(authOptions);