From 2faaec41d79dd69f3f880b7675fcb8e5840103fc Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Wed, 27 Sep 2023 05:07:31 +0000 Subject: [PATCH 1/2] 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 | 68 ++++++++++++++++++++++++++++++----- src/types/next-auth.d.ts | 1 + 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/features/auth/auth-api.ts b/src/features/auth/auth-api.ts index 73fbc0cb6..e1f73b31c 100644 --- a/src/features/auth/auth-api.ts +++ b/src/features/auth/auth-api.ts @@ -8,7 +8,8 @@ import { hashValue } from "./helpers"; 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( @@ -18,7 +19,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; } @@ -36,13 +38,60 @@ 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 + const 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) { + const body = await response.json() as { value?: string[] } + const 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 + } + } + } + else { + const body = await response.text() + throw new Error(`Bad response from POST /me/getMemberObjects: ${response.status} ${response.statusText}: ${body}`) + } + } + 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; } @@ -89,15 +138,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 } } From e7e07263bf6089d9d4964a389248025533382297 Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Wed, 27 Sep 2023 20:30:35 +0000 Subject: [PATCH 2/2] docs: Add AZURE_AD_ALLOWED_PRINCIPALS doc --- docs/5-add-identity.md | 1 + docs/7-environment-variables.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/5-add-identity.md b/docs/5-add-identity.md index bf3d33206..095021efe 100644 --- a/docs/5-add-identity.md +++ b/docs/5-add-identity.md @@ -83,6 +83,7 @@ We'll create two GitHub apps: one for testing locally and another for production AZURE_AD_CLIENT_ID= AZURE_AD_CLIENT_SECRET= AZURE_AD_TENANT_ID= +AZURE_AD_ALLOWED_PRINCIPALS= ``` ## Configure an admin user diff --git a/docs/7-environment-variables.md b/docs/7-environment-variables.md index 0accbe5b8..7da761892 100644 --- a/docs/7-environment-variables.md +++ b/docs/7-environment-variables.md @@ -17,6 +17,7 @@ Below are the required environment variables, to be added to the Azure Portal or | `AZURE_AD_CLIENT_ID` | | The client id specific to the application | | `AZURE_AD_CLIENT_SECRET` | | The client secret specific to the application | | `AZURE_AD_TENANT_ID` | | The organisation Tenant ID | +| `AZURE_AD_ALLOWED_PRINCIPALS` | | Comma separated list of Object IDs for users/groups that are allowed to log in. **All authenticated users are allowed by default** | | `ADMIN_EMAIL_ADDRESS` | | Comma separated list of email addresses of the admin users ID | | **Azure Cognitive Search is optional. This is only required for chat over file feature.** | | `AZURE_SEARCH_API_KEY` | | API Key of Azure Cognitive search |