Skip to content

Commit 1dd218c

Browse files
committed
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.
1 parent 7826a9d commit 1dd218c

File tree

2 files changed

+57
-8
lines changed

2 files changed

+57
-8
lines changed

src/features/auth/auth-api.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import GitHubProvider from "next-auth/providers/github";
66
const configureIdentityProvider = () => {
77
const providers: Array<Provider> = [];
88

9-
const adminEmails = process.env.ADMIN_EMAIL_ADDRESS?.split(",").map(email => email.toLowerCase().trim());
9+
const adminEmails = process.env.ADMIN_EMAIL_ADDRESS?.split(",").map(email => email.toLowerCase().trim()).filter(email => email);
10+
const azureAdAllowedPrincipals = process.env.AZURE_AD_ALLOWED_PRINCIPALS?.split(",").map(oid => oid.toLowerCase().trim()).filter(oid => oid);
1011

1112
if (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) {
1213
providers.push(
@@ -16,7 +17,8 @@ const configureIdentityProvider = () => {
1617
async profile(profile) {
1718
const newProfile = {
1819
...profile,
19-
isAdmin: adminEmails?.includes(profile.email.toLowerCase())
20+
isAdmin: adminEmails?.includes(profile.email.toLowerCase()),
21+
isAllowed: true
2022
}
2123
return newProfile;
2224
}
@@ -34,13 +36,56 @@ const configureIdentityProvider = () => {
3436
clientId: process.env.AZURE_AD_CLIENT_ID!,
3537
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
3638
tenantId: process.env.AZURE_AD_TENANT_ID!,
37-
async profile(profile) {
38-
39+
authorization: {
40+
params: {
41+
// Add User.Read to reach the /me endpoint of Microsoft Graph
42+
scope: 'email openid profile User.Read'
43+
}
44+
},
45+
async profile(profile, tokens) {
46+
let isAllowed = true
47+
if (Array.isArray(azureAdAllowedPrincipals) && azureAdAllowedPrincipals.length > 0) {
48+
try {
49+
isAllowed = false
50+
// POST https://graph.microsoft.com/v1.0/me/getMemberObjects
51+
// It returns all IDs of principal objects which "me" is a member of (transitive)
52+
// https://learn.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects?view=graph-rest-1.0&tabs=http
53+
var response = await fetch(
54+
'https://graph.microsoft.com/v1.0/me/getMemberObjects',
55+
{
56+
method: 'POST',
57+
headers: {
58+
Authorization: `Bearer ${tokens.access_token}`,
59+
'Content-Type': 'application/json'
60+
},
61+
body: '{"securityEnabledOnly":true}'
62+
}
63+
)
64+
if (response.ok) {
65+
var body = await response.json() as { value?: string[] }
66+
var oids = body.value ?? []
67+
if (profile.oid) {
68+
// Append the object ID of user principal "me"
69+
oids.push(profile.oid)
70+
}
71+
for (const principal of azureAdAllowedPrincipals) {
72+
if (oids.includes(principal)) {
73+
isAllowed = true
74+
break
75+
}
76+
}
77+
}
78+
}
79+
catch (e) {
80+
console.log(e)
81+
}
82+
}
3983
const newProfile = {
4084
...profile,
4185
// throws error without this - unsure of the root cause (https://stackoverflow.com/questions/76244244/profile-id-is-missing-in-google-oauth-profile-response-nextauth)
4286
id: profile.sub,
43-
isAdmin: adminEmails?.includes(profile.email.toLowerCase()) || adminEmails?.includes(profile.preferred_username.toLowerCase())
87+
isAdmin: adminEmails?.includes(profile.email.toLowerCase()) || adminEmails?.includes(profile.preferred_username.toLowerCase()),
88+
isAllowed
4489
}
4590
return newProfile;
4691
}
@@ -54,15 +99,18 @@ export const options: NextAuthOptions = {
5499
secret: process.env.NEXTAUTH_SECRET,
55100
providers: [...configureIdentityProvider()],
56101
callbacks: {
57-
async jwt({token, user, account, profile, isNewUser, session}) {
102+
async jwt({ token, user, account, profile, isNewUser, session }) {
58103
if (user?.isAdmin) {
59-
token.isAdmin = user.isAdmin
104+
token.isAdmin = user.isAdmin
60105
}
61106
return token
62107
},
63-
async session({session, token, user }) {
108+
async session({ session, token, user }) {
64109
session.user.isAdmin = token.isAdmin as string
65110
return session
111+
},
112+
async signIn({ user }) {
113+
return user.isAllowed
66114
}
67115
},
68116
session: {

src/types/next-auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ declare module "next-auth" {
1212

1313
interface User {
1414
isAdmin: string
15+
isAllowed: boolean
1516
}
1617

1718
}

0 commit comments

Comments
 (0)