Skip to content

Commit 62c7ce9

Browse files
committed
update readme, handle cae
1 parent c37c4e0 commit 62c7ce9

File tree

13 files changed

+226
-73
lines changed

13 files changed

+226
-73
lines changed

2-WebApp-graph-user/2-6-BFF-Proxy/AppCreationScripts/sample.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"HomePage": "https://localhost:7000",
2828
"ReplyUrls": "https://localhost:7000/api/auth/signin-oidc, https://localhost:7000/api/auth/signout-oidc",
2929
"SDK": "MicrosoftIdentityWeb",
30-
"SampleSubPath": "2-WebApp-graph-user\\2-6-BFF-Proxy",
30+
"SampleSubPath": "2-WebApp-graph-user\\2-6-BFF-Proxy\\CallGraphBFF",
3131
"PasswordCredentials": "Auto",
3232
"Certificate": "Auto",
3333
"RequiredResourcesAccess": [

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/ClientApp/src/AuthProvider.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const AuthProviderHOC = (C) =>
1515
await this.getAccount();
1616
}
1717

18-
login = (postLoginRedirectUri, claimsChallenge) => {
18+
login = (postLoginRedirectUri, scopesToConsent) => {
1919
let url = "api/auth/login";
2020

2121
const searchParams = new URLSearchParams({});
@@ -24,8 +24,8 @@ const AuthProviderHOC = (C) =>
2424
searchParams.append('postLoginRedirectUri', encodeURIComponent(postLoginRedirectUri));
2525
}
2626

27-
if (claimsChallenge) {
28-
searchParams.append('claimsChallenge', JSON.stringify(claimsChallenge));
27+
if (scopesToConsent) {
28+
searchParams.append('scopesToConsent', scopesToConsent.join(' '));
2929
}
3030

3131
url = `${url}?${searchParams.toString()}`;
@@ -36,12 +36,12 @@ const AuthProviderHOC = (C) =>
3636
logout = (postLogoutRedirectUri) => {
3737
this.setState({ isAuthenticated: false, account: null });
3838

39-
let url = "api/auth/login";
39+
let url = "api/auth/logout";
4040

4141
const searchParams = new URLSearchParams({});
4242

4343
if (postLogoutRedirectUri) {
44-
searchParams.append('postLoginRedirectUri', encodeURIComponent(postLogoutRedirectUri));
44+
searchParams.append('postLogoutRedirectUri', encodeURIComponent(postLogoutRedirectUri));
4545
}
4646

4747
url = `${url}?${searchParams.toString()}`;

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/ClientApp/src/components/FetchGraph.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ export class FetchGraph extends Component {
4242
const data = await response.json();
4343
this.setState({ profile: data, loading: false });
4444
} else if (response.status === 401) {
45-
if (response.body) {
46-
const claims = await response.json();
47-
this.props.login(window.location.href, claims);
48-
}
4945
this.props.login(window.location.href);
5046
}
5147
} catch (error) {

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/Controllers/AuthController.cs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,51 @@
1-
using System.Security.Claims;
2-
using System.Web;
1+
using System.Web;
2+
using System.Security.Claims;
33
using Microsoft.AspNetCore.Authentication;
44
using Microsoft.AspNetCore.Authentication.Cookies;
55
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
66
using Microsoft.AspNetCore.Authorization;
77
using Microsoft.AspNetCore.Mvc;
8-
using static Microsoft.Graph.Constants;
98

109
namespace TodoListBFF.Controllers;
1110

1211
[Route("api/[controller]")]
1312
public class AuthController : Controller
1413
{
1514
[HttpGet("login")]
16-
public ActionResult Login(string? postLoginRedirectUri, string? claimsChallenge)
15+
public ActionResult Login(string? postLoginRedirectUri)
1716
{
1817
string redirectUri = !string.IsNullOrEmpty(postLoginRedirectUri) ? HttpUtility
1918
.UrlDecode(postLoginRedirectUri) : "/";
2019

20+
string claims = HttpContext.Session.GetString("claimsChallenge") ?? "";
21+
2122
var props = new AuthenticationProperties { RedirectUri = redirectUri };
2223

23-
if (claimsChallenge != null)
24+
if (!string.IsNullOrEmpty(claims))
2425
{
25-
string jsonString = claimsChallenge
26-
.Replace("\\", "")
27-
.Trim(new char[1] { '"' });
28-
29-
string? loginHint = (this.User.Identity as ClaimsIdentity)?.Claims
30-
.FirstOrDefault(c => c.Type == "login_hint")?.Value;
31-
32-
props.Items["claims"] = jsonString;
33-
props.Items["login_hint"] = loginHint;
26+
props.Items["claims"] = claims; // attach the challenge to the request
27+
HttpContext.Session.Remove("claimsChallenge"); // discard the challenge in session
3428
}
3529

3630
return Challenge(props);
3731
}
3832

3933
[Authorize]
4034
[HttpGet("logout")]
41-
public async Task<ActionResult> Logout()
35+
public async Task<ActionResult> Logout(string? postLogoutRedirectUri)
4236
{
43-
await HttpContext.SignOutAsync();
37+
string redirectUri = !string.IsNullOrEmpty(postLogoutRedirectUri) ? HttpUtility
38+
.UrlDecode(postLogoutRedirectUri) : "/";
4439

45-
var props = new AuthenticationProperties { RedirectUri = "/" };
40+
var props = new AuthenticationProperties { RedirectUri = redirectUri };
4641

42+
// Sign out from both cookie and OIDC authentication schemes
4743
List<string> optionList = new List<string> {
4844
CookieAuthenticationDefaults.AuthenticationScheme,
4945
OpenIdConnectDefaults.AuthenticationScheme
5046
};
5147

48+
await HttpContext.SignOutAsync();
5249
return new SignOutResult(optionList, props);
5350
}
5451

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/Controllers/ProfileController.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ public async Task<ActionResult<User>> GetProfile()
3333
catch (ServiceException svcex)
3434
when (svcex.InnerException is MicrosoftIdentityWebChallengeUserException)
3535
{
36-
return Unauthorized("MicrosoftIdentityWebChallengeUserException occurred.\n" + svcex.Message);
36+
return Unauthorized("MicrosoftIdentityWebChallengeUserException occurred\n" + svcex.Message);
3737
}
3838
catch (ServiceException svcex)
3939
when (svcex.Message.Contains("Continuous access evaluation"))
4040
{
41-
string claimChallenge = WwwAuthenticateParameters
41+
string claimsChallenge = WwwAuthenticateParameters
4242
.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
4343

44-
return Unauthorized(claimChallenge);
44+
// Set the claims challenge string to session, which will be used during the next login request
45+
HttpContext.Session.SetString("claimsChallenge", claimsChallenge);
46+
47+
return Unauthorized("Continuous access evaluation resulted in claims challenge\n" + svcex.Message);
4548
}
4649
catch (Exception ex)
4750
{

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/Program.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,30 @@
1111
// This flag ensures that the ClaimsIdentity claims collection will be built from the claims in the token
1212
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
1313

14-
// Add services to the container.
14+
builder.Services.AddDistributedMemoryCache();
15+
16+
// Add Microsoft.Identity.Web services to the container.
1517
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration)
1618
.EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetSection("DownstreamApi:Scopes").Value.Split(' '))
1719
.AddMicrosoftGraph(builder.Configuration.GetValue<string>("DownstreamApi:BaseUrl"), builder.Configuration.GetValue<string>("DownstreamApi:Scopes"))
1820
.AddInMemoryTokenCaches();
1921

22+
// Add session for sharing non-sensitive strings between routes.
23+
builder.Services.AddSession(options =>
24+
{
25+
options.IdleTimeout = TimeSpan.FromSeconds(30);
26+
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
27+
options.Cookie.HttpOnly = true;
28+
options.Cookie.IsEssential = true;
29+
});
30+
31+
// Configure cookie properties for ASP.NET Core cookie authentication.
2032
builder.Services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme, options => {
2133
options.Cookie.SameSite = SameSiteMode.Strict;
2234
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
2335
options.Cookie.HttpOnly = true;
2436
options.Cookie.IsEssential = true;
25-
26-
options.Events = new RespondUnauthorizedWhenSessionCookieNotFoundEvents();
27-
options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents();
37+
options.Events = new CustomCookieAuthenticationEvents(); // modifies the behavior of certain cookie authentication events.
2838
});
2939

3040
builder.Services.AddControllersWithViews()
@@ -46,6 +56,8 @@
4656
app.UseAuthentication();
4757
app.UseAuthorization();
4858

59+
app.UseSession();
60+
4961
app.MapControllerRoute(
5062
name: "default",
5163
pattern: "{controller}/{action=Index}/{id?}");
Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
1-
using Microsoft.AspNetCore.Authentication.Cookies;
1+
using Microsoft.AspNetCore.Authentication;
2+
using Microsoft.AspNetCore.Authentication.Cookies;
23
using Microsoft.Identity.Client;
34
using Microsoft.Identity.Web;
5+
using System.Net;
46

5-
internal class RejectSessionCookieWhenAccountNotInCacheEvents : CookieAuthenticationEvents
7+
internal class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
68
{
9+
// Respond with 401 instead of redirect to IdP for unauthenticated attempts to access a secure route
10+
public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
11+
{
12+
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
13+
return Task.CompletedTask;
14+
}
15+
16+
public override Task RedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context)
17+
{
18+
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
19+
return Task.CompletedTask;
20+
}
21+
22+
// Reject authentication cookie if no tokens are found in the cache
723
public async override Task ValidatePrincipal(CookieValidatePrincipalContext context)
824
{
925
try
1026
{
1127
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
1228

1329
string token = await tokenAcquisition.GetAccessTokenForUserAsync(
14-
scopes: new[] { "user.read" },
30+
scopes: new[] { "User.Read" },
1531
user: context.Principal);
1632
}
1733
catch (MicrosoftIdentityWebChallengeUserException ex) when (AccountDoesNotExitInTokenCache(ex))
@@ -23,7 +39,7 @@ public async override Task ValidatePrincipal(CookieValidatePrincipalContext cont
2339
/// <summary>
2440
/// Is the exception thrown because there is no account in the token cache?
2541
/// </summary>
26-
/// <param name="ex">Exception thrown by <see cref="ITokenAcquisition"/>.GetTokenForXX methods.</param>
42+
/// <param name="ex">Exception thrown by <see cref="ITokenAcquisition"/>.GetTokenForX methods.</param>
2743
/// <returns>A boolean telling if the exception was about not having an account in the cache</returns>
2844
private static bool AccountDoesNotExitInTokenCache(MicrosoftIdentityWebChallengeUserException ex)
2945
{

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/Utils/RespondUnauthorizedWhenSessionCookieNotFoundEvents.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

2-WebApp-graph-user/2-6-BFF-Proxy/CallGraphBFF/appsettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"AzureAd": {
33
"Instance": "https://login.microsoftonline.com/",
4-
"TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
4+
"TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
55
"ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]",
66
"ClientSecret": "[Copy the client secret added to the app from the Azure portal]",
77
//"ClientCertificates": [
@@ -17,7 +17,7 @@
1717
},
1818
"DownstreamApi": {
1919
"BaseUrl": "https://graph.microsoft.com/v1.0",
20-
"Scopes": "user.read"
20+
"Scopes": "User.Read"
2121
},
2222
"Logging": {
2323
"LogLevel": {

0 commit comments

Comments
 (0)