Skip to content

Commit 0fa919a

Browse files
committed
sync reusable lib in asp.net core web app and web api tutorials
1 parent f060f80 commit 0fa919a

File tree

4 files changed

+78
-10
lines changed

4 files changed

+78
-10
lines changed

Microsoft.Identity.Web/Client/TokenAcquisition.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public async Task AddAccountToCacheFromAuthorizationCode(AuthorizationCodeReceiv
128128
// even if it's not done yet, so that it does not concurrently call the Token endpoint.
129129
context.HandleCodeRedemption();
130130

131-
var application = BuildConfidentialClientApplication(context.HttpContext, context.Principal);
131+
var application = GetOrBuildConfidentialClientApplication(context.HttpContext, context.Principal);
132132

133133
// Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it and will not send the OAuth 2.0 request in
134134
// case a further call to AcquireTokenByAuthorizationCodeAsync in the future for incremental consent (getting a code requesting more scopes)
@@ -164,7 +164,7 @@ public async Task<string> GetAccessTokenOnBehalfOfUser(HttpContext context, IEnu
164164
throw new ArgumentNullException(nameof(scopes));
165165

166166
// Use MSAL to get the right token to call the API
167-
var application = BuildConfidentialClientApplication(context, context.User);
167+
var application = GetOrBuildConfidentialClientApplication(context, context.User);
168168

169169
// Case of a lazy OBO
170170
Claim jwtClaim = context.User.FindFirst("jwt");
@@ -262,7 +262,7 @@ public void AddAccountToCacheFromJwt(AspNetCore.Authentication.OpenIdConnect.Tok
262262
public async Task RemoveAccount(RedirectContext context)
263263
{
264264
ClaimsPrincipal user = context.HttpContext.User;
265-
IConfidentialClientApplication app = BuildConfidentialClientApplication(context.HttpContext, user);
265+
IConfidentialClientApplication app = GetOrBuildConfidentialClientApplication(context.HttpContext, user);
266266
IAccount account = await app.GetAccountAsync(context.HttpContext.User.GetMsalAccountId());
267267

268268
// Workaround for the guest account
@@ -280,6 +280,23 @@ public async Task RemoveAccount(RedirectContext context)
280280
}
281281
}
282282

283+
IConfidentialClientApplication application;
284+
285+
/// <summary>
286+
/// Creates an MSAL Confidential client application if needed
287+
/// </summary>
288+
/// <param name="httpContext"></param>
289+
/// <param name="claimsPrincipal"></param>
290+
/// <returns></returns>
291+
private IConfidentialClientApplication GetOrBuildConfidentialClientApplication(HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
292+
{
293+
if (application == null)
294+
{
295+
application = BuildConfidentialClientApplication(httpContext, claimsPrincipal);
296+
}
297+
return application;
298+
}
299+
283300
/// <summary>
284301
/// Creates an MSAL Confidential client application
285302
/// </summary>
@@ -376,7 +393,8 @@ private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityTok
376393
IEnumerable<string> requestedScopes;
377394
if (jwtToken != null)
378395
{
379-
userAssertion = new UserAssertion(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
396+
string rawData = (jwtToken.InnerToken != null) ? jwtToken.InnerToken.RawData : jwtToken.RawData;
397+
userAssertion = new UserAssertion(rawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
380398
requestedScopes = scopes ?? jwtToken.Audiences.Select(a => $"{a}/.default");
381399
}
382400
else
@@ -385,7 +403,7 @@ private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityTok
385403
// TODO: Understand if we could support other kind of client assertions (SAML);
386404
}
387405

388-
var application = BuildConfidentialClientApplication(httpContext, principal);
406+
var application = GetOrBuildConfidentialClientApplication(httpContext, principal);
389407

390408
// .Result to make sure that the cache is filled-in before the controller tries to get access tokens
391409
var result = application.AcquireTokenOnBehalfOf(requestedScopes.Except(scopesRequestedByMsalNet), userAssertion)

Microsoft.Identity.Web/Client/TokenCacheProviders/InMemory/MSALPerUserMemoryTokenCacheProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public MSALPerUserMemoryTokenCacheProvider(IMemoryCache cache, MSALMemoryTokenCa
5656
this.memoryCache = cache;
5757
this.httpContextAccessor = httpContextAccessor;
5858

59-
if (option != null)
59+
if (option == null)
6060
{
6161
this.CacheOptions = new MSALMemoryTokenCacheOptions();
6262
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Net;
7+
using System.Net.Http;
8+
using System.Security.Claims;
9+
using System.Text;
10+
11+
namespace Microsoft.Identity.Web.Resource
12+
{
13+
public static class ScopesRequiredByWebAPIExtension
14+
{
15+
/// <summary>
16+
/// When applied to an <see cref="HttpContext"/>, verifies that the user authenticated in the Web API has any of the
17+
/// accepted scopes. If the authentication user does not have any of these <paramref name="acceptedScopes"/>, the
18+
/// method throws an HTTP Unauthorized with the message telling which scopes are expected in the token
19+
/// </summary>
20+
/// <param name="acceptedScopes">Scopes accepted by this API</param>
21+
/// <exception cref="HttpRequestException"/> with a <see cref="HttpResponse.StatusCode"/> set to
22+
/// <see cref="HttpStatusCode.Unauthorized"/>
23+
public static void VerifyUserHasAnyAcceptedScope(this HttpContext context, params string[] acceptedScopes)
24+
{
25+
if (acceptedScopes == null)
26+
{
27+
throw new ArgumentNullException(nameof(acceptedScopes));
28+
}
29+
Claim scopeClaim = context?.User?.FindFirst("http://schemas.microsoft.com/identity/claims/scope");
30+
if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(acceptedScopes).Any())
31+
{
32+
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
33+
string message = $"The 'scope' claim does not contain scopes '{string.Join(",", acceptedScopes)}' or was not found";
34+
throw new HttpRequestException(message);
35+
}
36+
}
37+
}
38+
}

Microsoft.Identity.Web/WebApiStartupHelpers.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
using Microsoft.Extensions.DependencyInjection;
66
using Microsoft.Identity.Web.Client;
77
using Microsoft.Identity.Web.Resource;
8+
using Microsoft.IdentityModel.Tokens;
89
using System.Collections.Generic;
910
using System.IdentityModel.Tokens.Jwt;
1011
using System.Linq;
1112
using System.Security.Claims;
13+
using System.Security.Cryptography.X509Certificates;
1214
using System.Threading.Tasks;
1315

1416
namespace Microsoft.Identity.Web
@@ -22,7 +24,7 @@ public static class WebApiStartupHelpers
2224
/// <param name="services">Service collection to which to add authentication</param>
2325
/// <param name="configuration">Configuration</param>
2426
/// <returns></returns>
25-
public static IServiceCollection AddProtectWebApiWithMicrosoftIdentityPlatformV2(this IServiceCollection services, IConfiguration configuration)
27+
public static IServiceCollection AddProtectWebApiWithMicrosoftIdentityPlatformV2(this IServiceCollection services, IConfiguration configuration, X509Certificate2 tokenDecryptionCertificate = null)
2628
{
2729
services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
2830
.AddAzureADBearer(options => configuration.Bind("AzureAd", options));
@@ -45,6 +47,12 @@ public static IServiceCollection AddProtectWebApiWithMicrosoftIdentityPlatformV2
4547
// we inject our own multitenant validation logic (which even accepts both V1 and V2 tokens)
4648
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).ValidateAadIssuer;
4749

50+
// If you provide a token decryption certificate, it will be used to decrypt the token
51+
if (tokenDecryptionCertificate != null)
52+
{
53+
options.TokenValidationParameters.TokenDecryptionKey = new X509SecurityKey(tokenDecryptionCertificate);
54+
}
55+
4856
// When an access token for our own Web API is validated, we add it to MSAL.NET's cache so that it can
4957
// be used from the controllers.
5058
options.Events = new JwtBearerEvents();
@@ -66,14 +74,13 @@ public static IServiceCollection AddProtectWebApiWithMicrosoftIdentityPlatformV2
6674
/// will be kept with the user's claims until the API calls a downstream API. Otherwise the account for the
6775
/// user is immediately added to the token cache</param>
6876
/// <returns></returns>
69-
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes=null)
77+
public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> scopes = null)
7078
{
7179
services.AddTokenAcquisition();
7280
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
7381
{
7482
// If you don't pre-provide scopes when adding calling AddProtectedApiCallsWebApis, the On behalf of
7583
// flow will be delayed (lazy construction of MSAL's application
76-
7784
options.Events.OnTokenValidated = async context =>
7885
{
7986
if (scopes != null && scopes.Any())
@@ -87,7 +94,12 @@ public static IServiceCollection AddProtectedApiCallsWebApis(this IServiceCollec
8794
context.Success();
8895

8996
// Todo : rather use options.SaveToken?
90-
(context.Principal.Identity as ClaimsIdentity).AddClaim(new Claim("jwt", (context.SecurityToken as JwtSecurityToken).RawData));
97+
JwtSecurityToken jwtSecurityToken = context.SecurityToken as JwtSecurityToken;
98+
if (jwtSecurityToken != null)
99+
{
100+
string rawData = (jwtSecurityToken.InnerToken != null) ? jwtSecurityToken.InnerToken.RawData : jwtSecurityToken.RawData;
101+
(context.Principal.Identity as ClaimsIdentity).AddClaim(new Claim("jwt", rawData));
102+
}
91103
}
92104
// Adds the token to the cache, and also handles the incremental consent and claim challenges
93105
await Task.FromResult(0);

0 commit comments

Comments
 (0)