Skip to content

Commit b931371

Browse files
authored
Merge pull request #286 from Azure-Samples/jmprieur/webappwebapib2c
Update jennyf/b2cwebapi from latest jmprieur/webappwebapib2c (and therefore master)
2 parents ab84785 + f40c079 commit b931371

File tree

10 files changed

+150
-52
lines changed

10 files changed

+150
-52
lines changed

.gitignore

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
/2-WebApp-graph-user/2-1-Call-MSGraph/.vs
3636
/2-WebApp-graph-user/2-1-Call-MSGraph/bin
3737
/2-WebApp-graph-user/2-1-Call-MSGraph/obj
38+
/2-WebApp-graph-user/2-1-Call-MSGraph/TestResults
3839
/2-WebApp-graph-user/2-2-TokenCache/.vs
3940
/2-WebApp-graph-user/2-2-TokenCache/bin
4041
/2-WebApp-graph-user/2-2-TokenCache/obj
@@ -110,22 +111,16 @@
110111
/4-WebApp-your-API/Client/bin/Release/netcoreapp2.2
111112
/4-WebApp-your-API/Client/obj/Debug/netcoreapp2.2
112113
/4-WebApp-your-API/Client/obj/Release/netcoreapp2.2
113-
/Microsoft.Identity.Web.Test/obj/Release/netcoreapp2.2
114-
/Microsoft.Identity.Web.Test/obj/Debug/netcoreapp2.2
115-
/5-WebApp-AuthZ/5-2-Groups/bin/Release/netcoreapp2.2
116-
/5-WebApp-AuthZ/5-1-Roles/bin/Release/netcoreapp2.2
114+
/Microsoft.Identity.Web.Test/obj
115+
/Microsoft.Identity.Web.Test/bin
116+
/5-WebApp-AuthZ/5-2-Groups/bin
117+
/5-WebApp-AuthZ/5-1-Roles/bin
117118
/4-WebApp-your-API/TodoListService/obj
118119
/4-WebApp-your-API/TodoListService/bin
119120
/4-WebApp-your-API/Client/obj
120-
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin/Debug/netcoreapp2.2
121+
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin
121122
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/obj
122-
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin/Release/netcoreapp2.2
123-
/Microsoft.Identity.Web.Test/bin/Release/netcoreapp2.2
124123
/Microsoft.Identity.Web.Test/obj
125124
/4-WebApp-your-API/4-2-B2C/.vs
126125
/4-WebApp-your-API/4-2-B2C/Client/obj
127126
/4-WebApp-your-API/4-2-B2C/TodoListService/obj
128-
/2-WebApp-graph-user/2-3-Multi-Tenant/.vs/WebApp-OpenIDConnect-DotNet
129-
/2-WebApp-graph-user/2-3-Multi-Tenant/bin/Debug/netcoreapp2.2
130-
/2-WebApp-graph-user/2-3-Multi-Tenant/obj
131-
/Microsoft.Identity.Web.Test/bin/Release/netcoreapp3.0

2-WebApp-graph-user/2-1-Call-MSGraph/AspnetCoreWebApp-calls-Microsoft-Graph.sln

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp-OpenIDConnect-DotNet
77
EndProject
88
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web", "..\..\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj", "{E0CEF26A-6CE6-4505-851B-6580D5564752}"
99
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CD0BB564-6C1E-4FCE-B9AB-7C637FDEE569}"
11+
ProjectSection(SolutionItems) = preProject
12+
.editorconfig = .editorconfig
13+
EndProjectSection
14+
EndProject
15+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test", "..\..\Microsoft.Identity.Web.Test\Microsoft.Identity.Web.Test.csproj", "{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}"
16+
EndProject
1017
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.UI", "..\..\Microsoft.Identity.Web.UI\Microsoft.Identity.Web.UI.csproj", "{8CC22202-F66C-4332-A4F0-A2C09EBA08EC}"
11-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test", "..\..\Microsoft.Identity.Web.Test\Microsoft.Identity.Web.Test.csproj", "{0EEC3E2E-69D0-4A7F-98D6-4386330F4965}"
1218
EndProject
1319
Global
1420
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -24,14 +30,14 @@ Global
2430
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Debug|Any CPU.Build.0 = Debug|Any CPU
2531
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.ActiveCfg = Release|Any CPU
2632
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.Build.0 = Release|Any CPU
33+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
35+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
36+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.Build.0 = Release|Any CPU
2737
{8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2838
{8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
2939
{8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
3040
{8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Release|Any CPU.Build.0 = Release|Any CPU
31-
{0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32-
{0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Debug|Any CPU.Build.0 = Debug|Any CPU
33-
{0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Release|Any CPU.ActiveCfg = Release|Any CPU
34-
{0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Release|Any CPU.Build.0 = Release|Any CPU
3541
EndGlobalSection
3642
GlobalSection(SolutionProperties) = preSolution
3743
HideSolutionNode = FALSE

2-WebApp-graph-user/2-1-Call-MSGraph/WebApp-OpenIDConnect-DotNet.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</ItemGroup>
1919

2020
<ItemGroup>
21-
<PackageReference Include="Microsoft.Graph" Version="1.14.0" />
21+
<PackageReference Include="Microsoft.Graph" Version="1.21.0" />
2222
</ItemGroup>
2323

2424
<ItemGroup>

2-WebApp-graph-user/2-2-TokenCache/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ Note: if you had used the automation to setup your application mentioned in [Ste
175175

176176
Starting from the [previous phase of the tutorial](../../2-WebApp-graph-user/2-1-Call-MSGraph), the code was incrementally updated with the following steps:
177177

178+
### Reference Microsoft.Extensions.Caching.SqlServer
179+
180+
This sample proposes a distributed SQL token cache. To use it, you'll need to add a reference to the `Microsoft.Extensions.Caching.SqlServer` NuGet package
181+
178182
### Update the `Startup.cs` file to enable Token caching using Sql database.
179183

180184
```CSharp
@@ -205,6 +209,7 @@ The files `MSALAppSqlTokenCacheProvider.cs` and `MSALPerUserSqlTokenCacheProvide
205209
## Next steps
206210

207211
- Learn how to enable distributed caches in [token cache serialization](../2.2.%20token%20cache%20serialization)
212+
- Learn more about the [Distributed SQL Server Cache](https://docs.microsoft.com/aspnet/core/performance/caching/distributed#distributed-sql-server-cache)
208213
- Learn how the same principle you've just learnt can be used to call:
209214
- [several Microsoft APIs](../../3-WebApp-multi-APIs), which will enable you to learn how incremental consent and conditional access is managed in your Web App
210215
- 3rd party, or even [your own Web API](../../4-WebApp-your-API), which will enable you to learn about custom scopes

5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ public async Task<UserGroupsAndDirectoryRoles> GetCurrentUserGroupsAndRolesAsync
277277
public async Task<List<Group>> GetMyMemberOfGroupsAsync(string accessToken)
278278
{
279279
List<Group> groups = new List<Group>();
280-
280+
PrepareAuthenticatedClient(accessToken);
281281
// Get groups the current user is a direct member of.
282282
IUserMemberOfCollectionWithReferencesPage memberOfGroups = await graphServiceClient.Me.MemberOf.Request().GetAsync();
283283
if (memberOfGroups?.Count > 0)
@@ -378,4 +378,4 @@ await Task.Run(() =>
378378
}
379379
}
380380
}
381-
}
381+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.AspNetCore.Authentication.JwtBearer;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Text;
8+
using Xunit;
9+
using System.Linq;
10+
11+
namespace Microsoft.Identity.Web.Test
12+
{
13+
public class WebApiServiceCollectionExtensionsTests
14+
{
15+
[Fact]
16+
public void TestAuthority()
17+
{
18+
// Arrange
19+
JwtBearerOptions options = new JwtBearerOptions();
20+
21+
// Act and Assert
22+
options.Authority = "https://login.microsoftonline.com/common";
23+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
24+
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);
25+
26+
options.Authority = "https://login.microsoftonline.us/organizations";
27+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
28+
Assert.Equal("https://login.microsoftonline.us/organizations/v2.0", options.Authority);
29+
30+
options.Authority = "https://login.microsoftonline.com/common/";
31+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
32+
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);
33+
34+
options.Authority = "https://login.microsoftonline.com/common/v2.0";
35+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
36+
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);
37+
38+
39+
options.Authority = "https://login.microsoftonline.com/common/v2.0";
40+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
41+
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);
42+
43+
}
44+
45+
[Fact]
46+
public void TestAudience()
47+
{
48+
JwtBearerOptions options = new JwtBearerOptions();
49+
50+
// Act and Assert
51+
options.Audience = "https://localhost";
52+
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
53+
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1);
54+
Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "https://localhost");
55+
56+
options.Audience = "api://1EE5A092-0DFD-42B6-88E5-C517C0141321";
57+
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
58+
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1);
59+
Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "api://1EE5A092-0DFD-42B6-88E5-C517C0141321");
60+
61+
options.Audience = "1EE5A092-0DFD-42B6-88E5-C517C0141321";
62+
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
63+
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 2);
64+
Assert.Contains("api://1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences);
65+
Assert.Contains("1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences);
66+
67+
}
68+
}
69+
}

Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public override void OnException(ExceptionContext context)
4747
{
4848
string[] incrementalConsentScopes = new string[] { };
4949
MsalUiRequiredException msalUiRequiredException = context.Exception as MsalUiRequiredException;
50+
5051
if (msalUiRequiredException == null)
5152
{
5253
msalUiRequiredException = context.Exception?.InnerException as MsalUiRequiredException;
@@ -56,6 +57,9 @@ public override void OnException(ExceptionContext context)
5657
{
5758
if (CanBeSolvedByReSignInOfUser(msalUiRequiredException))
5859
{
60+
// Do not re-use the attribute param Scopes. For more info: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/273
61+
string[] scopes = null;
62+
5963
// the users cannot provide both scopes and ScopeKeySection at the same time
6064
if (!string.IsNullOrWhiteSpace(ScopeKeySection) && Scopes != null && Scopes.Length > 0)
6165
{
@@ -73,15 +77,17 @@ public override void OnException(ExceptionContext context)
7377
throw new InvalidOperationException($"The {nameof(ScopeKeySection)} is provided but the IConfiguration instance is not present in the services collection");
7478
}
7579

76-
incrementalConsentScopes = new string[] { configuration.GetValue<string>(ScopeKeySection) };
77-
}
78-
79-
if (Scopes != null && Scopes.Length > 0 && incrementalConsentScopes != null && incrementalConsentScopes.Length > 0)
80-
{
81-
throw new InvalidOperationException("no scopes provided here...");
80+
scopes = new string[] { configuration.GetValue<string>(ScopeKeySection) };
81+
82+
if (Scopes != null && Scopes.Length > 0 && scopes != null && scopes.Length > 0)
83+
{
84+
throw new InvalidOperationException("no scopes provided in scopes...");
85+
}
8286
}
87+
else
88+
scopes = Scopes;
8389

84-
var properties = BuildAuthenticationPropertiesForIncrementalConsent(incrementalConsentScopes, msalUiRequiredException, context.HttpContext);
90+
var properties = BuildAuthenticationPropertiesForIncrementalConsent(scopes, msalUiRequiredException, context.HttpContext);
8591
context.Result = new ChallengeResult(properties);
8692
}
8793
}

Microsoft.Identity.Web/Microsoft.Identity.Web.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</ItemGroup>
3535

3636
<ItemGroup Label="Build Tools" Condition="$([MSBuild]::IsOsPlatform('Windows'))">
37-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-18618-05" PrivateAssets="All" />
37+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
3838
</ItemGroup>
3939

4040

@@ -55,7 +55,8 @@
5555
<ItemGroup>
5656
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.1" />
5757
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.1" />
58-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.1" />
59-
<PackageReference Include="Microsoft.Identity.Client" Version="4.8.0" />
58+
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="3.1.1" />
59+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
60+
<PackageReference Include="Microsoft.Identity.Client" Version="4.8.1" />
6061
</ItemGroup>
6162
</Project>

Microsoft.Identity.Web/TokenAcquisition.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,22 +310,24 @@ private IConfidentialClientApplication BuildConfidentialClientApplication()
310310
request.PathBase,
311311
microsoftIdentityOptions.CallbackPath.Value ?? string.Empty);
312312

313-
string authority = string.Empty;
313+
if (!applicationOptions.Instance.EndsWith("/"))
314+
applicationOptions.Instance += "/";
315+
316+
string authority ;
314317
IConfidentialClientApplication app = null;
315318

316319
if (microsoftIdentityOptions.IsB2C)
317320
{
318-
authority = $"{applicationOptions.Instance.TrimEnd('/')}/tfp/{microsoftIdentityOptions.Domain}/{microsoftIdentityOptions.DefaultUserFlow}";
321+
authority = $"{applicationOptions.Instance}tfp/{microsoftIdentityOptions.Domain}/{microsoftIdentityOptions.DefaultUserFlow}";
319322
app = ConfidentialClientApplicationBuilder
320323
.CreateWithApplicationOptions(applicationOptions)
321324
.WithRedirectUri(currentUri)
322325
.WithB2CAuthority(authority)
323326
.Build();
324327
}
325-
326328
else
327329
{
328-
authority = $"{applicationOptions.Instance.TrimEnd('/')}/{applicationOptions.TenantId}/";
330+
authority = $"{applicationOptions.Instance}{applicationOptions.TenantId}/";
329331
app = ConfidentialClientApplicationBuilder
330332
.CreateWithApplicationOptions(applicationOptions)
331333
.WithRedirectUri(currentUri)

Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,12 @@ public static AuthenticationBuilder AddProtectedWebApi(
166166
if (string.IsNullOrWhiteSpace(options.Authority))
167167
options.Authority = AuthorityHelpers.BuildAuthority(microsoftIdentityOptions);
168168

169-
if (!AuthorityHelpers.IsV2Authority(options.Authority))
170-
options.Authority += "/v2.0";
169+
// This is an Microsoft identity platform Web API
170+
EnsureAuthorityIsV2_0(options);
171+
172+
// The valid audience could be given as Client Id or as Uri.
173+
// If it does not start with 'api://', this variant is added to the list of valid audiences.
174+
EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
171175

172176
options.TokenValidationParameters.ValidAudiences = GetValidAudiences(options, microsoftIdentityOptions);
173177

@@ -244,26 +248,36 @@ public static IServiceCollection AddProtectedWebApiCallsProtectedWebApi(
244248
return services;
245249
}
246250

247-
internal static List<string> GetValidAudiences(
248-
JwtBearerOptions options,
249-
MicrosoftIdentityOptions msIdentityOptions)
251+
/// <summary>
252+
/// Ensures that the authority is a v2.0 authority
253+
/// </summary>
254+
/// <param name="options">Jwt bearer options read from the config file
255+
/// or set by the developper, for which we want to ensure the authority
256+
/// is a v2.0 authority</param>
257+
internal static void EnsureAuthorityIsV2_0(JwtBearerOptions options)
250258
{
251-
var validAudiences = new List<string>();
259+
var authority = options.Authority.Trim().TrimEnd('/');
260+
if (!authority.EndsWith("v2.0"))
261+
authority += "/v2.0";
262+
options.Authority = authority;
263+
}
252264

253-
// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
254-
// If the developer doesn't set the Audience on JwtBearerOptions, use ClientId from MicrosoftIdentityOptions
255-
if (!string.IsNullOrEmpty(options.Audience))
256-
{
257-
validAudiences.Add(options.Audience);
265+
266+
/// <summary>
267+
/// Ensure that if the audience is a GUID, api://{audience} is also added
268+
/// as a valid audience (this is the default App ID URL in the app registration
269+
/// portal)
270+
/// </summary>
271+
/// <param name="options">Jwt bearer options for which to ensure that
272+
/// api://GUID is a valid audience</param>
273+
internal static void EnsureValidAudiencesContainsApiGuidIfGuidProvided(JwtBearerOptions options)
274+
{
275+
var validAudiences = new List<string> { options.Audience };
276+
if (!options.Audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase)
277+
&& Guid.TryParse(options.Audience, out _))
258278
validAudiences.Add($"api://{options.Audience}");
259-
}
260-
else
261-
{
262-
validAudiences.Add(msIdentityOptions.ClientId);
263-
validAudiences.Add($"api://{msIdentityOptions.ClientId}");
264-
}
265279

266-
return validAudiences;
280+
options.TokenValidationParameters.ValidAudiences = validAudiences;
267281
}
268282
}
269-
}
283+
}

0 commit comments

Comments
 (0)