diff --git a/src/features/auth/auth-api.ts b/src/features/auth/auth-api.ts index d3c010426..39a291036 100644 --- a/src/features/auth/auth-api.ts +++ b/src/features/auth/auth-api.ts @@ -6,7 +6,8 @@ import GitHubProvider from "next-auth/providers/github"; const configureIdentityProvider = () => { const providers: Array = []; - const adminEmails = process.env.ADMIN_EMAIL_ADDRESS?.split(",").map(email => email.toLowerCase().trim()); + const adminEmails = process.env.ADMIN_EMAIL_ADDRESS?.split(",").map(email => email.toLowerCase().trim()).filter(email => email); + const azureAdAllowedPrincipals = process.env.AZURE_AD_ALLOWED_PRINCIPALS?.split(",").map(oid => oid.toLowerCase().trim()).filter(oid => oid); if (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) { providers.push( @@ -16,7 +17,8 @@ const configureIdentityProvider = () => { async profile(profile) { const newProfile = { ...profile, - isAdmin: adminEmails?.includes(profile.email.toLowerCase()) + isAdmin: adminEmails?.includes(profile.email.toLowerCase()), + isAllowed: true } return newProfile; } @@ -34,13 +36,56 @@ const configureIdentityProvider = () => { clientId: process.env.AZURE_AD_CLIENT_ID!, clientSecret: process.env.AZURE_AD_CLIENT_SECRET!, tenantId: process.env.AZURE_AD_TENANT_ID!, - async profile(profile) { - + authorization: { + params: { + // Add User.Read to reach the /me endpoint of Microsoft Graph + scope: 'email openid profile User.Read' + } + }, + async profile(profile, tokens) { + let isAllowed = true + if (Array.isArray(azureAdAllowedPrincipals) && azureAdAllowedPrincipals.length > 0) { + try { + isAllowed = false + // POST https://graph.microsoft.com/v1.0/me/getMemberObjects + // It returns all IDs of principal objects which "me" is a member of (transitive) + // https://learn.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects?view=graph-rest-1.0&tabs=http + var response = await fetch( + 'https://graph.microsoft.com/v1.0/me/getMemberObjects', + { + method: 'POST', + headers: { + Authorization: `Bearer ${tokens.access_token}`, + 'Content-Type': 'application/json' + }, + body: '{"securityEnabledOnly":true}' + } + ) + if (response.ok) { + var body = await response.json() as { value?: string[] } + var oids = body.value ?? [] + if (profile.oid) { + // Append the object ID of user principal "me" + oids.push(profile.oid) + } + for (const principal of azureAdAllowedPrincipals) { + if (oids.includes(principal)) { + isAllowed = true + break + } + } + } + } + catch (e) { + console.log(e) + } + } const newProfile = { ...profile, // throws error without this - unsure of the root cause (https://stackoverflow.com/questions/76244244/profile-id-is-missing-in-google-oauth-profile-response-nextauth) id: profile.sub, - isAdmin: adminEmails?.includes(profile.email.toLowerCase()) || adminEmails?.includes(profile.preferred_username.toLowerCase()) + isAdmin: adminEmails?.includes(profile.email.toLowerCase()) || adminEmails?.includes(profile.preferred_username.toLowerCase()), + isAllowed } return newProfile; } @@ -54,15 +99,18 @@ export const options: NextAuthOptions = { secret: process.env.NEXTAUTH_SECRET, providers: [...configureIdentityProvider()], callbacks: { - async jwt({token, user, account, profile, isNewUser, session}) { + async jwt({ token, user, account, profile, isNewUser, session }) { if (user?.isAdmin) { - token.isAdmin = user.isAdmin + token.isAdmin = user.isAdmin } return token }, - async session({session, token, user }) { + async session({ session, token, user }) { session.user.isAdmin = token.isAdmin as string return session + }, + async signIn({ user }) { + return user.isAllowed } }, session: { diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 1e44d8162..e970276be 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -12,6 +12,7 @@ declare module "next-auth" { interface User { isAdmin: string + isAllowed: boolean } }