Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit 86fb16c

Browse files
committed
add caching in SPA
1 parent c82399e commit 86fb16c

File tree

18 files changed

+183
-111
lines changed

18 files changed

+183
-111
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
using Microsoft.AspNetCore.Authorization;
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Security.Claims;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authorization;
26
using Microsoft.AspNetCore.Http;
37
using Microsoft.AspNetCore.Mvc;
48
using Microsoft.EntityFrameworkCore;
59
using Microsoft.Identity.Web;
610
using Microsoft.Identity.Web.Resource;
7-
using System.Collections.Generic;
8-
using System.Linq;
9-
using System.Security.Claims;
10-
using System.Threading.Tasks;
1111
using TodoListAPI.Infrastructure;
1212
using TodoListAPI.Models;
1313

@@ -172,7 +172,7 @@ private async void PopulateDefaultToDos(string _currentPrincipalId)
172172
/// <returns></returns>
173173
private ClaimsPrincipal GetCurrentClaimsPrincipal()
174174
{
175-
// Irrespective of whether a user signs in or not, the AspNet security middle-ware dehydrates the claims in the
175+
// Irrespective of whether a user signs in or not, the AspNet security middleware dehydrates the claims in the
176176
// HttpContext.User.Claims collection
177177

178178
if (_contextAccessor.HttpContext != null && _contextAccessor.HttpContext.User != null)

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
using Microsoft.AspNetCore.Authentication.JwtBearer;
2-
using Microsoft.Extensions.Caching.Memory;
3-
using Microsoft.Extensions.DependencyInjection;
4-
using Microsoft.Graph;
5-
using System;
1+
using System;
62
using System.Collections.Generic;
73
using System.Diagnostics;
84
using System.IdentityModel.Tokens.Jwt;
95
using System.Linq;
106
using System.Security.Claims;
117
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Authentication.JwtBearer;
9+
using Microsoft.Extensions.Caching.Memory;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Graph;
1212

1313
namespace TodoListAPI.Utils
1414
{
@@ -35,12 +35,13 @@ public static async Task ProcessAnyGroupsOverage(TokenValidatedContext context)
3535

3636
// ensure MemoryCache is available
3737
_memoryCache = context.HttpContext.RequestServices.GetService<IMemoryCache>();
38+
3839
if (_memoryCache == null)
3940
{
4041
throw new ArgumentNullException("_memoryCache", "Memory cache is not available.");
4142
}
4243

43-
// Checks if the incoming token contained a 'Group Overage' claim.
44+
// Checks if the incoming token contains a 'Group Overage' claim.
4445
if (HasOverageOccurred(principal))
4546
{
4647
// Gets group values from cache if available.
@@ -71,7 +72,7 @@ public static async Task ProcessAnyGroupsOverage(TokenValidatedContext context)
7172
}
7273

7374
// 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.
74-
// 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.
75+
// 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.
7576
// For capturing real-time changes to a user's group membership, consider implementing MS Graph change notifications (https://learn.microsoft.com/graph/api/resources/webhooks)
7677
SaveUsersGroupsToCache(usergroups, principal);
7778
}
@@ -140,7 +141,7 @@ private static async Task<List<string>> ProcessUserGroupsForOverage(TokenValidat
140141
IUserMemberOfCollectionWithReferencesPage memberPage = new UserMemberOfCollectionWithReferencesPage();
141142
try
142143
{
143-
//Request to get groups and directory roles that the user is a direct member of.
144+
// Request to get groups and directory roles that the user is a direct member of.
144145
memberPage = await graphClient.Me.MemberOf.Request().Select(select).GetAsync().ConfigureAwait(false);
145146
}
146147
catch (Exception graphEx)
@@ -179,7 +180,7 @@ private static async Task<List<string>> ProcessUserGroupsForOverage(TokenValidat
179180
}
180181
else
181182
{
182-
throw new ArgumentNullException("SecurityToken", "Group membership cannot be fetched if no tken has been provided.");
183+
throw new ArgumentNullException("SecurityToken", "Group membership cannot be fetched if no token has been provided.");
183184
}
184185
}
185186
}
@@ -280,9 +281,8 @@ private static void SaveUsersGroupsToCache(List<string> usersGroups, ClaimsPrinc
280281

281282
Debug.WriteLine($"Adding users groups for '{cacheKey}'.");
282283

283-
// 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.
284+
// 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.
284285
// For capturing real-time changes to a user's group membership, consider implementing MS Graph change notifications (https://learn.microsoft.com/en-us/graph/api/resources/webhooks)
285-
286286
var cacheEntryOptions = new MemoryCacheEntryOptions()
287287
.SetSlidingExpiration(TimeSpan.FromSeconds(60))
288288
.SetAbsoluteExpiration(TimeSpan.FromSeconds(3600))

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

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
using System;
2+
using System.IdentityModel.Tokens.Jwt;
3+
using System.Linq;
4+
using System.Threading.Tasks;
15
using Microsoft.AspNetCore.Authentication.JwtBearer;
26
using Microsoft.AspNetCore.Builder;
37
using Microsoft.AspNetCore.Hosting;
@@ -7,10 +11,6 @@
711
using Microsoft.Extensions.Hosting;
812
using Microsoft.Identity.Web;
913
using Microsoft.IdentityModel.Logging;
10-
using System;
11-
using System.IdentityModel.Tokens.Jwt;
12-
using System.Linq;
13-
using System.Threading.Tasks;
1414
using TodoListAPI.Infrastructure;
1515
using TodoListAPI.Models;
1616
using TodoListAPI.Utils;
@@ -101,19 +101,8 @@ public void ConfigureServices(IServiceCollection services)
101101
services.AddControllers();
102102
services.AddHttpContextAccessor();
103103

104-
//Added for session state
105-
//services.AddDistributedMemoryCache();
106-
107-
//services.AddScoped<IGraphHelper, GraphHelper>();
108-
//services.AddScoped(typeof(ICollectionProcessor<>), typeof(CollectionProcessor<>));
109-
110-
//services.AddSession(options =>
111-
//{
112-
// options.IdleTimeout = TimeSpan.FromMinutes(10);
113-
//});
114-
115104
// The following flag can be used to get more descriptive errors in development environments
116-
// Enable diagnostic logging to help with troubleshooting. For more details, see https://aka.ms/IdentityModel/PII.
105+
// Enable diagnostic logging to help with troubleshooting. For more details, see https://aka.ms/IdentityModel/PII.
117106
// You might not want to keep this following flag on for production
118107
IdentityModelEventSource.ShowPII = true;
119108

@@ -150,7 +139,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
150139
app.UseCors("default");
151140
app.UseHttpsRedirection();
152141
app.UseRouting();
153-
//app.UseSession();
154142
app.UseAuthentication();
155143
app.UseAuthorization();
156144
app.UseEndpoints(endpoints =>

5-AccessControl/2-call-api-groups/README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ When attending to overage scenarios, which requires a call to [Microsoft Graph](
406406
407407
##### Angular GroupGuard service
408408

409-
Consider the [group.guard.ts](./SPA/src/app/group.guard.ts). Here, we are checking whether the token for the user's ID token has the `_claim_names` claim, which indicates that the user has too many group memberships. If so, we redirect the user to the [overage](./SPA/src/app/overage/overage.component.ts) component. There, we initiate a call to MS Graph API's `https://graph.microsoft.com/v1.0/me/memberOf` endpoint to query the full list of groups that the user belongs to. Finally we check for the designated `groupID` among this list.
409+
Consider the [group.guard.ts](./SPA/src/app/group.guard.ts). Here, we are checking whether the token for the user's ID token has the `_claim_names` claim, which indicates that the user has too many group memberships. If so, we redirect the user to the [overage](./SPA/src/app/overage/overage.component.ts) component. There, we initiate a call to MS Graph API's `https://graph.microsoft.com/v1.0/me/memberOf` endpoint to query the required list of groups that the user belongs to. Finally we check for the designated `groupID` among this list.
410410

411411
```typescript
412412
@Injectable()
@@ -423,7 +423,7 @@ export class GroupGuard extends BaseGuard {
423423
super(msalGuardConfig, msalBroadcastService, authService, location, router);
424424
}
425425

426-
override activateHelper(state?: RouterStateSnapshot, route?: ActivatedRouteSnapshot): Observable<boolean | UrlTree> {
426+
override activateHelper(state?: RouterStateSnapshot, route?: ActivatedRouteSnapshot): Observable<boolean | UrlTree> {
427427
let result = super.activateHelper(state, route);
428428

429429
const expectedGroups: string[] = route ? route.data['expectedGroups'] : [];
@@ -436,20 +436,25 @@ export class GroupGuard extends BaseGuard {
436436
activeAccount = this.authService.instance.getAllAccounts()[0] as AccountWithGroupClaims;
437437
}
438438

439-
if (!activeAccount?.idTokenClaims?.groups && this.graphService.getUser().groupIDs.length === 0) {
440-
if (activeAccount.idTokenClaims?._claim_names) {
441-
window.alert('You have too many group memberships. The application will now query Microsoft Graph to get the full list of groups that you are a member of.');
439+
// check either the ID token or a non-expired storage entry for the groups membership claim
440+
if (!activeAccount?.idTokenClaims?.groups && !checkGroupsInStorage(activeAccount)) {
441+
442+
if (activeAccount.idTokenClaims?._claim_names && activeAccount.idTokenClaims?._claim_names.groups) {
443+
window.alert('You have too many group memberships. The application will now query Microsoft Graph to check if you are a member of any of the groups required.');
442444
return this.router.navigate(['/overage']);
443445
}
444446

445447
window.alert('Token does not have groups claim. Please ensure that your account is assigned to a security group and then sign-out and sign-in again.');
446448
return of(false);
447449
}
448450

451+
/**
452+
* If an overage scenario occurs, the ID token will not have a groups claim. Instead, after
453+
* the overage is handled, the user object in the graphService will have the relevant group IDs.
454+
* If you like, you may want to cache the group IDs in sessionStorage as an alternative.
455+
*/
449456
const hasRequiredGroup = expectedGroups.some((group: string) =>
450-
activeAccount?.idTokenClaims?.groups?.includes(group)
451-
||
452-
this.graphService.getUser().groupIDs.includes(group)
457+
activeAccount?.idTokenClaims?.groups?.includes(group) || getGroupsFromStorage(activeAccount)?.includes(group)
453458
);
454459

455460
if (!hasRequiredGroup) {
@@ -509,7 +514,7 @@ const routes: Routes = [
509514

510515
#### .NET Core web API and how to handle the overage scenario
511516

512-
1. In [Startup.cs](./API/TodoListAPI/Startup.cs), `OnTokenValidated` event calls **GetSignedInUsersGroups** method defined in *GraphHelper.cs* to process groups overage claim.
517+
1. In [Startup.cs](./API/TodoListAPI/Startup.cs), `OnTokenValidated` event calls **GetSignedInUsersGroups** method defined in [GraphHelper.cs](./API//TodoListAPI/Services/GraphHelper.cs) to process groups overage claim.
513518

514519
```csharp
515520
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

5-AccessControl/2-call-api-groups/SPA/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

5-AccessControl/2-call-api-groups/SPA/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@angular-devkit/build-angular": "^14.1.0",
3232
"@angular/cli": "~14.0.5",
3333
"@angular/compiler-cli": "^14.0.0",
34+
"@microsoft/microsoft-graph-types": "^2.25.0",
3435
"@types/jasmine": "~4.0.0",
3536
"jasmine-core": "~4.1.0",
3637
"karma": "~6.3.0",

5-AccessControl/2-call-api-groups/SPA/src/app/app.component.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { AuthenticationResult, EventMessage, EventType, InteractionStatus, Inter
44
import { Subject } from 'rxjs';
55
import { filter, takeUntil } from 'rxjs/operators';
66

7+
import { clearGroupsInStorage } from './utils/storage-utils';
8+
79
@Component({
810
selector: 'app-root',
911
templateUrl: './app.component.html',
@@ -28,8 +30,8 @@ export class AppComponent implements OnInit, OnDestroy {
2830
this.authService.instance.enableAccountStorageEvents(); // Optional - This will enable ACCOUNT_ADDED and ACCOUNT_REMOVED events emitted when a user logs in or out of another tab or window
2931

3032
/**
31-
* You can subscribe to MSAL events as shown below. For more info,
32-
* visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/events.md
33+
* You can subscribe to MSAL events as shown below. For more information, visit:
34+
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/events.md
3335
*/
3436

3537
this.msalBroadcastService.msalSubject$
@@ -97,7 +99,18 @@ export class AppComponent implements OnInit, OnDestroy {
9799
}
98100

99101
logout() {
100-
this.authService.logout();
102+
const activeAccount = this.authService.instance.getActiveAccount() || this.authService.instance.getAllAccounts()[0];
103+
clearGroupsInStorage(activeAccount); // make sure to remove groups from storage
104+
105+
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
106+
this.authService.logoutPopup({
107+
account: activeAccount
108+
});
109+
} else {
110+
this.authService.logoutRedirect({
111+
account: activeAccount
112+
});
113+
}
101114
}
102115

103116
// unsubscribe to events when component is destroyed

5-AccessControl/2-call-api-groups/SPA/src/app/dashboard/dashboard.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnInit } from '@angular/core';
22
import { TodoService } from './../todo.service';
3-
import { Todo } from '../todo';
3+
import { Todo } from '../todo.service';
44

55
@Component({
66
selector: 'app-dashboard',

5-AccessControl/2-call-api-groups/SPA/src/app/graph.service.ts

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,15 @@ import { Injectable } from '@angular/core';
33
import { AuthenticationResult, InteractionRequiredAuthError } from '@azure/msal-browser';
44
import { MsalService } from '@azure/msal-angular';
55
import { Client, PageCollection, PageIterator, PageIteratorCallback } from '@microsoft/microsoft-graph-client';
6+
import { DirectoryObject } from '@microsoft/microsoft-graph-types';
67

7-
import { User } from './user';
88
import { protectedResources } from './auth-config';
99

1010
@Injectable({
1111
providedIn: 'root'
1212
})
1313
export class GraphService {
1414

15-
private user: User = {
16-
displayName: "",
17-
groupIDs: [],
18-
};
19-
2015
constructor(private authService: MsalService) { }
2116

2217
private getGraphClient(accessToken: string) {
@@ -33,15 +28,17 @@ export class GraphService {
3328

3429
private async getToken(scopes: string[]): Promise<string> {
3530
let authResponse: AuthenticationResult | null = null;
36-
31+
3732
try {
3833
authResponse = await this.authService.instance.acquireTokenSilent({
3934
account: this.authService.instance.getActiveAccount()!,
4035
scopes: scopes,
4136
});
42-
37+
4338
} catch (error) {
4439
if (error instanceof InteractionRequiredAuthError) {
40+
// TODO: get default interaction type from auth config
41+
4542
authResponse = await this.authService.instance.acquireTokenPopup({
4643
scopes: protectedResources.apiGraph.scopes,
4744
});
@@ -53,43 +50,38 @@ export class GraphService {
5350
return authResponse ? authResponse.accessToken : "";
5451
}
5552

56-
getUser(): User {
57-
return this.user;
58-
};
59-
60-
setGroups(groups: any): void {
61-
this.user.groupIDs = groups;
62-
};
63-
64-
async getGroups(): Promise<string[]> {
65-
const allGroups: string[] = [];
53+
async getFilteredGroups(filterGroups: string[] = []): Promise<string[]> {
54+
const groups: string[] = [];
6655

6756
try {
6857
const accessToken = await this.getToken(protectedResources.apiGraph.scopes);
6958

7059
// Get a graph client instance for the given access token
7160
const graphClient = this.getGraphClient(accessToken);
7261

73-
// Makes request to fetch mails list. Which is expected to have multiple pages of data.
74-
let response: PageCollection = await graphClient.api(protectedResources.apiGraph.endpoint).get();
62+
const selectQuery = "id,displayName,onPremisesNetBiosName,onPremisesDomainName,onPremisesSamAccountNameonPremisesSecurityIdentifier";
7563

64+
// Makes request to fetch groups list, which is expected to have multiple pages of data.
65+
let response: PageCollection = await graphClient.api(protectedResources.apiGraph.endpoint).select(selectQuery).get();
66+
7667
// A callback function to be called for every item in the collection. This call back should return boolean indicating whether not to continue the iteration process.
77-
let callback: PageIteratorCallback = (data) => {
78-
allGroups.push(data.id); // Add the group id to the groups array
68+
let callback: PageIteratorCallback = (data: DirectoryObject) => {
69+
if (data.id && filterGroups.includes(data.id)) groups.push(data.id); // Add the group id to the groups array
70+
if (filterGroups.filter(x => !groups.includes(x)).length === 0) return false; // Stop iterating if all the required groups are found
7971
return true;
8072
};
81-
73+
8274
// Creating a new page iterator instance with client a graph client instance, page collection response from request and callback
8375
let pageIterator = new PageIterator(graphClient, response, callback);
84-
76+
8577
// This iterates the collection until the nextLink is drained out.
8678
await pageIterator.iterate();
8779

88-
return allGroups;
80+
return groups;
8981
} catch (error) {
9082
console.log(error);
9183
}
9284

93-
return allGroups;
85+
return groups;
9486
}
9587
}

0 commit comments

Comments
 (0)