1
1
using Microsoft . AspNetCore . Authentication . OpenIdConnect ;
2
+ using Microsoft . AspNetCore . Authorization ;
3
+ using Microsoft . AspNetCore . Http ;
2
4
using Microsoft . Extensions . DependencyInjection ;
3
5
using Microsoft . Graph ;
4
6
using System ;
@@ -14,86 +16,134 @@ namespace WebApp_OpenIDConnect_DotNet.Services
14
16
public class GraphHelper
15
17
{
16
18
/// <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.
19
22
/// </summary>
20
23
/// <param name="context">TokenValidatedContext</param>
21
24
public static async Task < List < string > > GetSignedInUsersGroups ( TokenValidatedContext context )
22
25
{
23
26
List < string > groupClaims = new List < string > ( ) ;
24
27
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 > ( ) ;
25
78
try
26
79
{
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 )
29
85
{
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
+ }
32
88
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" ) )
34
96
{
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 ) ;
36
101
}
37
102
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" ;
51
105
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
+ }
54
117
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 ) ;
66
122
67
- if ( memberPage ? . Count > 0 )
123
+ if ( allgroups ? . Count > 0 )
68
124
{
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 ;
71
126
72
- if ( allgroups ? . Count > 0 )
127
+ if ( identity != null )
73
128
{
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" ) )
77
133
{
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 )
82
136
{
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 ) ;
96
143
}
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 ) ;
97
147
}
98
148
}
99
149
}
@@ -164,5 +214,34 @@ private static List<Group> ProcessIGraphServiceMemberOfCollectionPage(IUserMembe
164
214
}
165
215
return allGroups ;
166
216
}
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
+ }
167
246
}
168
247
}
0 commit comments