Skip to content

Commit 97b114b

Browse files
committed
update graph api endpoint for fetching groups
1 parent 536ff11 commit 97b114b

File tree

6 files changed

+62
-144
lines changed

6 files changed

+62
-144
lines changed

5-AccessControl/2-call-api-groups/API/TodoListAPI/Services/GraphHelper.cs

Lines changed: 29 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,8 @@ public static async Task ProcessAnyGroupsOverage(TokenValidatedContext context,
6262
// Remove any existing 'groups' claim
6363
RemoveExistingGroupsClaims(identity);
6464

65-
// Re-populate the `groups` claim with the complete list of groups fetched from MS Graph
66-
foreach (var group in usergroups)
67-
{
68-
// The following code adds group ids to the 'groups' claim. But depending upon your requirement and the format of the 'groups' claim selected in
69-
// the app registration, you might want to add other attributes than id to the `groups` claim, examples being;
70-
71-
// For instance if the required format is 'NetBIOSDomain\sAMAccountName' then the code is as commented below:
72-
// identity.AddClaim(new Claim("groups", group.OnPremisesNetBiosName+"\\"+group.OnPremisesSamAccountName));
73-
identity.AddClaim(new Claim("groups", group));
74-
}
65+
// And re-populate
66+
RepopulateGroupsClaim(usergroups, identity);
7567

7668
// Here we add the groups in a cache variable so that calls to Graph can be minimized to fetch all the groups for a user.
7769
// IMPORTANT: Group list is cached for 1 hr by default, and thus cached groups will miss any changes to a users group membership for this duration.
@@ -109,7 +101,7 @@ private static bool IsAccessToken(ClaimsIdentity identity)
109101
/// <param name="context">TokenValidatedContext</param>
110102
private static async Task<List<string>> ProcessUserGroupsForOverage(TokenValidatedContext context, List<string> requiredGroupIds)
111103
{
112-
var allgroups = new List<Group>();
104+
var allgroups = new List<string>();
113105

114106
try
115107
{
@@ -135,55 +127,42 @@ private static async Task<List<string>> ProcessUserGroupsForOverage(TokenValidat
135127
context.HttpContext.Items.Add(Cached_Graph_Token_Key, context.SecurityToken as JwtSecurityToken);
136128
}
137129

138-
// MS Graph call to fetch the entire set of group membership..
139-
140-
// The properties that we want to retrieve from MemberOf endpoint.
141-
string select = "id,displayName,onPremisesNetBiosName,onPremisesDomainName,onPremisesSamAccountNameonPremisesSecurityIdentifier";
142-
143-
IUserMemberOfCollectionWithReferencesPage memberPage = new UserMemberOfCollectionWithReferencesPage();
144-
145130
try
146131
{
147132
// Request to get groups and directory roles that the user is a direct member of.
148-
memberPage = await graphClient.Me.MemberOf.Request().Select(select).GetAsync().ConfigureAwait(false);
149-
}
150-
catch (Exception graphEx)
151-
{
152-
var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message;
153-
Debug.WriteLine("Call to Microsoft Graph failed: " + exMsg);
154-
}
133+
var memberPage = await graphClient.Me.CheckMemberGroups(requiredGroupIds).Request().PostAsync().ConfigureAwait(false);
134+
allgroups = memberPage.ToList<string>();
155135

156-
if (memberPage?.Count > 0)
157-
{
158-
// There is a limit to number of groups returned in a page, so the method below make further calls to Microsoft graph to get all the groups.
159-
allgroups = ProcessIGraphServiceMemberOfCollectionPage(memberPage, requiredGroupIds);
136+
// There is a limit to number of groups returned in a page, so the method below make further calls to Microsoft graph to get all the groups.
137+
// allgroups = ProcessIGraphServiceMemberOfCollectionPage(memberPage, requiredGroupIds);
160138

161-
if (allgroups?.Count > 0)
162-
{
163-
var principal = context.Principal;
164-
165-
if (principal != null)
139+
if (allgroups?.Count > 0)
166140
{
167-
var identity = principal.Identity as ClaimsIdentity;
141+
var principal = context.Principal;
168142

169-
// Checks if token is for protected APIs i.e., if token is 'Access Token'.
170-
if (IsAccessToken(identity))
143+
if (principal != null)
171144
{
172-
// Remove existing groups claims
173-
RemoveExistingGroupsClaims(identity);
145+
var identity = principal.Identity as ClaimsIdentity;
146+
147+
// Checks if token is for protected APIs i.e., if token is 'Access Token'.
148+
if (IsAccessToken(identity))
149+
{
150+
// Remove existing groups claims
151+
RemoveExistingGroupsClaims(identity);
174152

175-
// And re-populate
176-
RepopulateGroupsClaim(allgroups, identity);
153+
// And re-populate
154+
RepopulateGroupsClaim(allgroups, identity);
155+
}
177156
}
178-
}
179157

180-
// return the full list of security groups
181-
return allgroups.Select(x => x.Id).ToList();
182-
}
158+
// return the full list of security groups
159+
return allgroups;
160+
}
183161
}
184-
else
162+
catch (Exception graphEx)
185163
{
186-
throw new ArgumentNullException("SecurityToken", "Group membership cannot be fetched if no token has been provided.");
164+
var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message;
165+
Debug.WriteLine("Call to Microsoft Graph failed: " + exMsg);
187166
}
188167
}
189168
}
@@ -212,16 +191,16 @@ private static async Task<List<string>> ProcessUserGroupsForOverage(TokenValidat
212191
/// <param name="allgroups">The user's entire security group membership.</param>
213192
/// <param name="identity">The identity.</param>
214193
/// <autogeneratedoc />
215-
private static void RepopulateGroupsClaim(List<Group> allgroups, ClaimsIdentity identity)
194+
private static void RepopulateGroupsClaim(List<string> allgroups, ClaimsIdentity identity)
216195
{
217-
foreach (Group group in allgroups)
196+
foreach (string group in allgroups)
218197
{
219198
// The following code adds group ids to the 'groups' claim. But depending upon your requirement and the format of the 'groups' claim selected in
220199
// the app registration, you might want to add other attributes than id to the `groups` claim, examples being;
221200

222201
// For instance if the required format is 'NetBIOSDomain\sAMAccountName' then the code is as commented below:
223202
// identity.AddClaim(new Claim("groups", group.OnPremisesNetBiosName+"\\"+group.OnPremisesSamAccountName));
224-
identity.AddClaim(new Claim("groups", group.Id));
203+
identity.AddClaim(new Claim("groups", group));
225204
}
226205
}
227206

@@ -294,52 +273,5 @@ private static void SaveUsersGroupsToCache(List<string> usersGroups, ClaimsPrinc
294273

295274
_memoryCache.Set(cacheKey, usersGroups, cacheEntryOptions);
296275
}
297-
298-
/// <summary>
299-
/// Paginates through the group membership page and fetches all subsequent pages to retrieve the entire list of a user's group membership
300-
/// </summary>
301-
/// <param name="membersCollectionPage">First page having collection of directory roles and groups</param>
302-
/// <returns>List of groups</returns>
303-
private static List<Group> ProcessIGraphServiceMemberOfCollectionPage(IUserMemberOfCollectionWithReferencesPage membersCollectionPage, List<string> requiredGroupIds)
304-
{
305-
List<Group> allGroups = new List<Group>();
306-
307-
try
308-
{
309-
if (membersCollectionPage != null)
310-
{
311-
do
312-
{
313-
// Page through results
314-
foreach (DirectoryObject directoryObject in membersCollectionPage.CurrentPage)
315-
{
316-
// Collection contains directory roles and groups of the user.
317-
// Checks and adds groups only to the list.
318-
if (directoryObject is Group && requiredGroupIds.Contains(directoryObject.Id))
319-
{
320-
allGroups.Add(directoryObject as Group);
321-
}
322-
}
323-
324-
// Are there more pages (i.e has a @odata.nextLink) AND did we already found all the required groups
325-
if (membersCollectionPage.NextPageRequest != null && allGroups.Count() != requiredGroupIds.Count())
326-
{
327-
membersCollectionPage = membersCollectionPage.NextPageRequest.GetAsync().Result;
328-
}
329-
else
330-
{
331-
membersCollectionPage = null;
332-
}
333-
} while (membersCollectionPage != null);
334-
}
335-
}
336-
catch (ServiceException ex)
337-
{
338-
Console.WriteLine($"We could not process the groups page: {ex}");
339-
return null;
340-
}
341-
342-
return allGroups;
343-
}
344276
}
345277
}

5-AccessControl/2-call-api-groups/API/TodoListAPI/Startup.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,25 @@ namespace TodoListAPI
2020
{
2121
public class Startup
2222
{
23+
public List<string> allowedClientApps;
24+
public List<string> requiredGroupsIds;
25+
public CacheSettings cacheSettings;
2326
public IConfiguration Configuration { get; }
2427

2528
public Startup(IConfiguration configuration)
2629
{
2730
Configuration = configuration;
31+
32+
allowedClientApps = new List<string>() { Configuration["AzureAd:ClientId"] };
33+
34+
requiredGroupsIds = Configuration.GetSection("AzureAd:Groups")
35+
.AsEnumerable().Select(x => x.Value).Where(x => x != null).ToList();
36+
37+
cacheSettings = new CacheSettings
38+
{
39+
SlidingExpirationInSeconds = Configuration.GetValue<string>("CacheSettings:SlidingExpirationInSeconds"),
40+
AbsoluteExpirationInSeconds = Configuration.GetValue<string>("CacheSettings:AbsoluteExpirationInSeconds")
41+
};
2842
}
2943

3044
// This method gets called by the runtime. Use this method to add services to the container.
@@ -62,24 +76,13 @@ public void ConfigureServices(IServiceCollection services)
6276

6377
options.Events.OnTokenValidated = async context =>
6478
{
65-
string[] allowedClientApps = { Configuration["AzureAd:ClientId"] };
66-
67-
List<string> requiredGroupsIds = Configuration.GetSection("AzureAd:Groups")
68-
.AsEnumerable().Select(x => x.Value).Where(x => x != null).ToList();
69-
70-
CacheSettings cacheSettings = new CacheSettings
71-
{
72-
SlidingExpirationInSeconds = Configuration.GetValue<string>("CacheSettings:SlidingExpirationInSeconds"),
73-
AbsoluteExpirationInSeconds = Configuration.GetValue<string>("CacheSettings:AbsoluteExpirationInSeconds")
74-
};
75-
7679
string clientAppId = context?.Principal?.Claims
7780
.FirstOrDefault(x => x.Type == "azp" || x.Type == "appid")?.Value;
7881

7982
// In this scenario, client and service (API) share the same clientId and we disallow all calls to this API, except from the SPA
8083
if (!allowedClientApps.Contains(clientAppId))
8184
{
82-
throw new Exception("This client is not authorized to call this Api");
85+
throw new Exception("This client is not authorized to call this API");
8386
}
8487

8588
if (context != null)

5-AccessControl/2-call-api-groups/API/TodoListAPI/appsettings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"TenantId": "Enter the ID of your Azure AD tenant copied from the Azure portal",
55
"ClientId": "Enter the application ID (clientId) of the 'TodoListAPI' application copied from the Azure portal",
66
"ClientSecret": "Enter the Client Secret of the 'TodoListAPI' application copied from the Azure portal",
7-
"ClientCapabilities": [ "cp1" ],
87
"Scopes": [ "access_via_group_assignments" ],
98
"Groups": {
109
"GroupAdmin": "Enter the object ID for GroupAdmin group copied from Azure Portal",

0 commit comments

Comments
 (0)