From 1dd218c7f49eafe5daa4106e280a8e386add5967 Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Wed, 27 Sep 2023 05:07:31 +0000 Subject: [PATCH] src: Add AZURE_AD_ALLOWED_PRINCIPALS support New environment variable AZURE_AD_ALLOWED_PRINCIPALS is a comma separated list of Object IDs for Azure AD user/group principals that are allowed to log in to the app. If the list is empty, all authenticated users are allowed to log in. This feature utilizes the Microsoft Graph API endpoint /me/getMemberObjects. --- src/features/auth/auth-api.ts | 64 ++++++++++++++++++++++++++++++----- src/types/next-auth.d.ts | 1 + 2 files changed, 57 insertions(+), 8 deletions(-) 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 } }