Skip to content

Commit 5813ff2

Browse files
committed
Code Refactoring
1 parent 8968223 commit 5813ff2

File tree

2 files changed

+159
-79
lines changed

2 files changed

+159
-79
lines changed

5-WebApp-AuthZ/5-2-Groups/Infrastructure/CustomAuthorization.cs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Threading.Tasks;
8+
using WebApp_OpenIDConnect_DotNet.Services;
9+
810
namespace WebApp_OpenIDConnect_DotNet.Infrastructure
911
{
12+
/// <summary>
13+
/// GroupPolicyHandler represents Policy-based authorization.
14+
/// GroupPolicyHandler evaluates the GroupPolicyRequirement against AuthorizationHandlerContext
15+
/// to determine if authorization is allowed.
16+
/// </summary>
1017
public class GroupPolicyHandler : AuthorizationHandler<GroupPolicyRequirement>
1118
{
1219
private IHttpContextAccessor _httpContextAccessor;
@@ -15,35 +22,29 @@ public GroupPolicyHandler(IHttpContextAccessor httpContextAccessor)
1522
{
1623
_httpContextAccessor = httpContextAccessor;
1724
}
25+
26+
/// <summary>
27+
/// Makes a decision if authorization is allowed based on GroupPolicyRequirement.
28+
/// </summary>
29+
/// <param name="context"></param>
30+
/// <param name="requirement"></param>
31+
/// <returns></returns>
1832
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
1933
GroupPolicyRequirement requirement)
2034
{
21-
// Checks if groups claim exists in claims collection of signed-in User.
22-
if (context.User.Claims.Any(x => x.Type == "groups"))
35+
// Calls method to check if requirement exists in user claims or session.
36+
if (GraphHelper.CheckUsersGroupMembership(context, requirement.GroupName, _httpContextAccessor))
2337
{
24-
if (context.User.Claims.Any(x => x.Type == "groups" && x.Value == requirement.GroupName))
25-
{
26-
context.Succeed(requirement);
27-
}
28-
return Task.CompletedTask;
29-
}
30-
31-
// Checks if Session contains data for groupClaims.
32-
// The data will exist for 'Group Overage' claim.
33-
else if (_httpContextAccessor.HttpContext.Session.Keys.Contains("groupClaims"))
34-
{
35-
// Retrieves all the groups saved in Session.
36-
var groups = _httpContextAccessor.HttpContext.Session.GetAsByteArray("groupClaims") as List<string>;
37-
38-
// Checks if required group exists in Session.
39-
if (groups?.Count > 0 && groups.Contains(requirement.GroupName))
40-
{
41-
context.Succeed(requirement);
42-
}
38+
context.Succeed(requirement);
4339
}
4440
return Task.CompletedTask;
4541
}
4642
}
43+
44+
/// <summary>
45+
/// GroupPolicyRequirement contains data parameter that
46+
/// GroupPolicyHandler uses to evaluate against the current user principal or session data.
47+
/// </summary>
4748
public class GroupPolicyRequirement : IAuthorizationRequirement
4849
{
4950
public string GroupName { get; }

5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/GraphHelper.cs

Lines changed: 137 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Http;
24
using Microsoft.Extensions.DependencyInjection;
35
using Microsoft.Graph;
46
using System;
@@ -14,86 +16,134 @@ namespace WebApp_OpenIDConnect_DotNet.Services
1416
public class GraphHelper
1517
{
1618
/// <summary>
17-
/// This method inspects the claims collection created from the ID or Access token issued to a user and returns the groups that are present in the token . If it detects groups overage,
18-
/// the method then makes calls to Microsoft Graph to fetch the group membership of the authenticated user.
19+
/// This method inspects the claims collection created from the ID or Access token issued to a user and returns the groups that are present in the token.
20+
/// If groups claims are already present in Session then it returns the list of groups by calling GetSessionGroupList method.
21+
/// If it detects groups overage, the method then makes calls to ProcessUserGroupsForOverage method.
1922
/// </summary>
2023
/// <param name="context">TokenValidatedContext</param>
2124
public static async Task<List<string>> GetSignedInUsersGroups(TokenValidatedContext context)
2225
{
2326
List<string> groupClaims = new List<string>();
2427

28+
//
29+
groupClaims = GetSessionGroupList(context.HttpContext.Session);
30+
if (groupClaims?.Count>0)
31+
{
32+
return groupClaims;
33+
}
34+
// Checks if the incoming token contained a 'Group Overage' claim.
35+
else if (HasOverageOccurred(context.Principal))
36+
{
37+
groupClaims= await ProcessUserGroupsForOverage(context);
38+
}
39+
return groupClaims;
40+
}
41+
42+
/// <summary>
43+
/// Retrieves all the groups saved in Session.
44+
/// </summary>
45+
/// <param name="_httpContextSession"></param>
46+
/// <returns></returns>
47+
private static List<string> GetSessionGroupList(ISession _httpContextSession)
48+
{
49+
// Checks if Session contains data for groupClaims.
50+
// The data will exist for 'Group Overage' claim.
51+
if (_httpContextSession.Keys.Contains("groupClaims"))
52+
{
53+
return _httpContextSession.GetAsByteArray("groupClaims") as List<string>;
54+
}
55+
return null;
56+
}
57+
58+
/// <summary>
59+
/// Checks if 'Group Overage' claim exists for signed-in user.
60+
/// </summary>
61+
/// <param name="identity"></param>
62+
/// <returns></returns>
63+
private static bool HasOverageOccurred(ClaimsPrincipal identity)
64+
{
65+
return identity.Claims.Any(x => x.Type == "hasgroups" || (x.Type == "_claim_names" && x.Value == "{\"groups\":\"src1\"}"));
66+
}
67+
68+
69+
/// <summary>
70+
/// This method is called for Groups overage scenario.
71+
/// The method makes calls to Microsoft Graph to fetch the group membership of the authenticated user.
72+
/// </summary>
73+
/// <param name="context"></param>
74+
/// <returns></returns>
75+
static async Task<List<string>> ProcessUserGroupsForOverage(TokenValidatedContext context)
76+
{
77+
List<string> groupClaims = new List<string>();
2578
try
2679
{
27-
// Checks if the incoming token contained a 'Group Overage' claim.
28-
if (context.Principal.Claims.Any(x => x.Type == "hasgroups" || (x.Type == "_claim_names" && x.Value == "{\"groups\":\"src1\"}")))
80+
81+
// Before instatntiating GraphServiceClient, the app should have granted admin consent for 'GroupMember.Read.All' permission.
82+
var graphClient = context.HttpContext.RequestServices.GetService<GraphServiceClient>();
83+
84+
if (graphClient == null)
2985
{
30-
// Before instatntiating GraphServiceClient, the app should have granted admin consent for 'GroupMember.Read.All' permission.
31-
var graphClient = context.HttpContext.RequestServices.GetService<GraphServiceClient>();
86+
Console.WriteLine("No service for type 'Microsoft.Graph.GraphServiceClient' has been registered in the Startup.");
87+
}
3288

33-
if (graphClient == null)
89+
// Checks if the SecurityToken is not null.
90+
// For the Web App, SecurityToken contains value of the ID Token.
91+
else if (context.SecurityToken != null)
92+
{
93+
// Checks if 'JwtSecurityTokenUsedToCallWebAPI' key already exists.
94+
// This key is required to acquire Access Token for Graph Service Client.
95+
if (!context.HttpContext.Items.ContainsKey("JwtSecurityTokenUsedToCallWebAPI"))
3496
{
35-
Console.WriteLine("No service for type 'Microsoft.Graph.GraphServiceClient' has been registered in the Startup.");
97+
// For Web App, access token is retrieved using account identifier. But at this point account identifier is null.
98+
// So, SecurityToken is saved in 'JwtSecurityTokenUsedToCallWebAPI' key.
99+
// The key is then used to get the Access Token on-behalf of user.
100+
context.HttpContext.Items.Add("JwtSecurityTokenUsedToCallWebAPI", context.SecurityToken as JwtSecurityToken);
36101
}
37102

38-
// Checks if the SecurityToken is not null.
39-
// For the Web App, SecurityToken contains value of the ID Token.
40-
else if (context.SecurityToken != null)
41-
{
42-
// Checks if 'JwtSecurityTokenUsedToCallWebAPI' key already exists.
43-
// This key is required to acquire Access Token for Graph Service Client.
44-
if (!context.HttpContext.Items.ContainsKey("JwtSecurityTokenUsedToCallWebAPI"))
45-
{
46-
// For Web App, access token is retrieved using account identifier. But at this point account identifier is null.
47-
// So, SecurityToken is saved in 'JwtSecurityTokenUsedToCallWebAPI' key.
48-
// The key is then used to get the Access Token on-behalf of user.
49-
context.HttpContext.Items.Add("JwtSecurityTokenUsedToCallWebAPI", context.SecurityToken as JwtSecurityToken);
50-
}
103+
// The properties that we want to retrieve from MemberOf endpoint.
104+
string select = "id,displayName,onPremisesNetBiosName,onPremisesDomainName,onPremisesSamAccountNameonPremisesSecurityIdentifier";
51105

52-
// The properties that we want to retrieve from MemberOf endpoint.
53-
string select = "id,displayName,onPremisesNetBiosName,onPremisesDomainName,onPremisesSamAccountNameonPremisesSecurityIdentifier";
106+
IUserMemberOfCollectionWithReferencesPage memberPage = new UserMemberOfCollectionWithReferencesPage();
107+
try
108+
{
109+
//Request to get groups and directory roles that the user is a direct member of.
110+
memberPage = await graphClient.Me.MemberOf.Request().Select(select).GetAsync().ConfigureAwait(false);
111+
}
112+
catch (Exception graphEx)
113+
{
114+
var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message;
115+
Console.WriteLine("Call to Microsoft Graph failed: " + exMsg);
116+
}
54117

55-
IUserMemberOfCollectionWithReferencesPage memberPage = new UserMemberOfCollectionWithReferencesPage();
56-
try
57-
{
58-
//Request to get groups and directory roles that the user is a direct member of.
59-
memberPage = await graphClient.Me.MemberOf.Request().Select(select).GetAsync().ConfigureAwait(false);
60-
}
61-
catch (Exception graphEx)
62-
{
63-
var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message;
64-
Console.WriteLine("Call to Microsoft Graph failed: " + exMsg);
65-
}
118+
if (memberPage?.Count > 0)
119+
{
120+
// There is a limit to number of groups returned, below method make calls to Microsoft graph to get all the groups.
121+
var allgroups = ProcessIGraphServiceMemberOfCollectionPage(memberPage);
66122

67-
if (memberPage?.Count > 0)
123+
if (allgroups?.Count > 0)
68124
{
69-
// There is a limit to number of groups returned, below method make calls to Microsoft graph to get all the groups.
70-
var allgroups = ProcessIGraphServiceMemberOfCollectionPage(memberPage);
125+
var identity = (ClaimsIdentity)context.Principal.Identity;
71126

72-
if (allgroups?.Count > 0)
127+
if (identity != null)
73128
{
74-
var identity = (ClaimsIdentity)context.Principal.Identity;
75-
76-
if (identity != null)
129+
// Checks if token is 'ID Token'.
130+
// ID Token does not contain 'aapid' or 'azp' claims.
131+
// These claims exist for Access Token.
132+
if (!identity.Claims.Any(x => x.Type == "appid" || x.Type == "azp"))
77133
{
78-
// Checks if token is 'ID Token'.
79-
// ID Token does not contain 'aapid' or 'azp' claims.
80-
// These claims exist for Access Token.
81-
if (!identity.Claims.Any(x => x.Type == "appid" || x.Type == "azp"))
134+
// Re-populate the `groups` claim with the complete list of groups fetched from MS Graph
135+
foreach (Group group in allgroups)
82136
{
83-
// Re-populate the `groups` claim with the complete list of groups fetched from MS Graph
84-
foreach (Group group in allgroups)
85-
{
86-
// The following code adds group ids to the 'groups' claim. But depending upon your reequirement and the format of the 'groups' claim selected in
87-
// the app registration, you might want to add other attributes than id to the `groups` claim, examples being;
88-
89-
// For instance if the required format is 'NetBIOSDomain\sAMAccountName' then the code is as commented below:
90-
// groupClaims.Add(group.OnPremisesNetBiosName+"\\"+group.OnPremisesSamAccountName));
91-
groupClaims.Add(group.Id);
92-
}
93-
94-
// Here we add the groups in a session variable that is used in authorization policy handler.
95-
context.HttpContext.Session.SetAsByteArray("groupClaims", groupClaims);
137+
// The following code adds group ids to the 'groups' claim. But depending upon your reequirement and the format of the 'groups' claim selected in
138+
// the app registration, you might want to add other attributes than id to the `groups` claim, examples being;
139+
140+
// For instance if the required format is 'NetBIOSDomain\sAMAccountName' then the code is as commented below:
141+
// groupClaims.Add(group.OnPremisesNetBiosName+"\\"+group.OnPremisesSamAccountName));
142+
groupClaims.Add(group.Id);
96143
}
144+
145+
// Here we add the groups in a session variable that is used in authorization policy handler.
146+
context.HttpContext.Session.SetAsByteArray("groupClaims", groupClaims);
97147
}
98148
}
99149
}
@@ -164,5 +214,34 @@ private static List<Group> ProcessIGraphServiceMemberOfCollectionPage(IUserMembe
164214
}
165215
return allGroups;
166216
}
217+
218+
/// <summary>
219+
/// Checks if user is member of the required group.
220+
/// </summary>
221+
/// <param name="context"></param>
222+
/// <param name="GroupName"></param>
223+
/// <param name="_httpContextAccessor"></param>
224+
/// <returns></returns>
225+
public static bool CheckUsersGroupMembership(AuthorizationHandlerContext context, string GroupName, IHttpContextAccessor _httpContextAccessor)
226+
{
227+
bool result = false;
228+
// Checks if groups claim exists in claims collection of signed-in User.
229+
if(HasOverageOccurred(context.User))
230+
{
231+
// Calls method GetSessionGroupList to get groups from session.
232+
var groups = GetSessionGroupList(_httpContextAccessor.HttpContext.Session);
233+
234+
// Checks if required group exists in Session.
235+
if (groups?.Count > 0 && groups.Contains(GroupName))
236+
{
237+
result = true;
238+
}
239+
}
240+
else if (context.User.Claims.Any(x => x.Type == "groups" && x.Value == GroupName))
241+
{
242+
result = true;
243+
}
244+
return result;
245+
}
167246
}
168247
}

0 commit comments

Comments
 (0)