Created
May 5, 2023 18:19
-
-
Save mcorkum/553fe738a6a427c89470a163c7d1a066 to your computer and use it in GitHub Desktop.
Microsoft ADFS Provider for Resource in Next Auth with Types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the required packages | |
import NextAuth, { NextAuthOptions } from 'next-auth'; | |
import { JWT } from 'next-auth/jwt'; | |
import jwt_decode from 'jwt-decode'; // Used for decoding the accessToken in account | |
// Define the types for the profile | |
interface ProfileProps { | |
aud: string; | |
iss: string; | |
iat: number; | |
nbf: number; | |
exp: number; | |
auth_time: number; | |
sub: string; | |
upn: string; | |
unique_name: string; | |
sid: string; | |
} | |
// Extend existing types for NextAuth | |
declare module 'next-auth' { | |
// Add an error property to the Session interface | |
export interface Session { | |
error: 'RefreshAccessTokenError' | unknown; | |
} | |
// Add the expires_at property to the Account interface | |
export interface Account { | |
expires_at: any; | |
} | |
} | |
// Define a new interface to extend the JWT interface | |
interface ExtendedTokenSet extends JWT { | |
expires_at: number; | |
} | |
// Extend the JWT interface from next-auth/jwt | |
declare module 'next-auth/jwt' { | |
interface JWT { | |
access_token?: string; // Optional access token property | |
expires_at: number; | |
refresh_token?: string; // Optional refresh token property | |
error?: 'RefreshAccessTokenError'; // Optional error property | |
} | |
} | |
// For more information on each option (and a full list of options) go to https://next-auth.js.org/configuration/options | |
export const authOptions: NextAuthOptions = { | |
// cufed configuration options | |
providers: [ | |
{ | |
clientId: process.env.NEXT_CU_CLIENT_ID, | |
clientSecret: process.env.NEXT_CU_CLIENT_SECRET, | |
id: 'xxx', | |
name: 'xxxx', | |
type: 'oauth', | |
token: process.env.OAUTH_URL + '/adfs/oauth2/token', | |
userinfo: process.env.OAUTH_URL + '/adfs/oauth2/', | |
issuer: process.env.OAUTH_ISSUER, | |
wellKnown: | |
process.env.OAUTH_URL + '/adfs/.well-known/openid-configuration', | |
idToken: true, | |
authorization: { | |
url: process.env.OAUTH_URL + '/adfs/oauth2/authorize/', | |
params: { | |
scope: process.env.OAUTH_SCOPE, | |
resource: process.env.OAUTH_RESOURCE, | |
}, | |
}, | |
profile(profile: ProfileProps) { | |
console.log( | |
'π ~ file: [...nextauth].ts:72 ~ profile ~ profile:', | |
profile | |
); | |
return { | |
id: profile.sid, | |
name: profile.unique_name, | |
email: profile.upn, | |
}; | |
}, | |
}, | |
], | |
callbacks: { | |
// This is where "token" in the application is set. | |
// Account is only here when cufed first authenticates. | |
async jwt({ token, user, account }) { | |
console.log('π ~ file: [...nextauth].ts:88 ~ jwt ~ token:', token); | |
if (account) { | |
console.log('π ~ file: [...nextauth].ts:87 ~ jwt ~ account:', account); | |
// Save the access token and refresh token in the JWT on the initial login | |
return { | |
name: user?.name, // Store the user's name | |
email: user?.email, // Store the user's email | |
access_token: account.access_token, | |
expires_at: Math.floor(Date.now() / 1000 + account.expires_at), | |
refresh_token: account.refresh_token, | |
}; | |
} else if (Date.now() < token.expires_at * 1000) { | |
// If the access token has not expired yet, return it | |
console.log('π ~ file: [...nextauth].ts:100', 'Token has not expired'); | |
return token; | |
} else { | |
// If the access token has expired, try to refresh it | |
console.log( | |
'π ~ file: [...nextauth].ts:100', | |
'Token has expired - refreshing' | |
); | |
try { | |
// Build the URL string to send to cufed | |
const params = new URLSearchParams({ | |
client_id: process.env.NEXT_CU_CLIENT_ID ?? '', | |
client_secret: process.env.NEXT_CU_CLIENT_SECRET ?? '', | |
grant_type: 'refresh_token', | |
refresh_token: token.refresh_token ?? '', | |
}); | |
// Ask cufed for a refresh token from the token endpoint | |
const response: Response = await fetch( | |
`${process.env.OAUTH_URL}/adfs/oauth2/token`, | |
{ | |
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
body: params.toString(), | |
method: 'POST', | |
} | |
); | |
// Store both tokens in a set | |
const tokens: ExtendedTokenSet = await response.json(); | |
console.log( | |
'π ~ file: [...nextauth].ts:129 ~ jwt ~ tokens:', | |
tokens | |
); | |
// if there's a problem with the refresh token, throw the | |
if (!response.ok) throw tokens; | |
return { | |
...token, // Keep the previous token properties | |
name: user?.name, // Store the user's name | |
email: user?.email, // Store the user's email | |
access_token: tokens.access_token, | |
expires_at: Math.floor(Date.now() / 1000 + tokens.expires_at), | |
refresh_token: tokens.refresh_token ?? token.refresh_token, // Fall back to old refresh token | |
}; | |
} catch (error) { | |
console.error('Error refreshing access token', error); | |
// The error property will be used client-side to handle the refresh token error | |
return { ...token, error: 'RefreshAccessTokenError' as const }; | |
} | |
} | |
}, | |
// This is where session is being set. You can assign values from the token here. | |
async session({ session, token }) { | |
// Assign user properties from the token to the session | |
if (token) { | |
session.user = { | |
name: token.name, | |
email: token.email, | |
}; | |
session.error = token.error; | |
} | |
console.log( | |
'π ~ file: [...nextauth].ts:169 ~ session ~ session:', | |
session | |
); | |
return session; | |
}, | |
}, | |
}; | |
export default NextAuth(authOptions); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment