11using Microsoft . AspNetCore . Authentication . OpenIdConnect ;
2+ using Microsoft . AspNetCore . Authorization ;
3+ using Microsoft . AspNetCore . Http ;
24using Microsoft . Extensions . DependencyInjection ;
35using Microsoft . Graph ;
46using 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