Skip to content

Commit 9ec821d

Browse files
committed
Adding unit tests to #278
and reverting the UI package upgrade as 3.1.1 has a bug
1 parent c2249d8 commit 9ec821d

File tree

6 files changed

+121
-30
lines changed

6 files changed

+121
-30
lines changed

.gitignore

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
/2-WebApp-graph-user/2-1-Call-MSGraph/.vs
3030
/2-WebApp-graph-user/2-1-Call-MSGraph/bin
3131
/2-WebApp-graph-user/2-1-Call-MSGraph/obj
32+
/2-WebApp-graph-user/2-1-Call-MSGraph/TestResults
3233
/2-WebApp-graph-user/2-2-TokenCache/.vs
3334
/2-WebApp-graph-user/2-2-TokenCache/bin
3435
/2-WebApp-graph-user/2-2-TokenCache/obj
@@ -114,11 +115,8 @@
114115
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin/Debug/netcoreapp2.2
115116
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/obj
116117
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin/Release/netcoreapp2.2
117-
/Microsoft.Identity.Web.Test/bin/Release/netcoreapp2.2
118+
/Microsoft.Identity.Web.Test/bin
118119
/Microsoft.Identity.Web.Test/obj
119120
/4-WebApp-your-API/4-2-B2C/.vs
120121
/4-WebApp-your-API/4-2-B2C/Client/obj
121122
/4-WebApp-your-API/4-2-B2C/TodoListService/obj
122-
/2-WebApp-graph-user/2-3-Multi-Tenant/.vs/WebApp-OpenIDConnect-DotNet
123-
/2-WebApp-graph-user/2-3-Multi-Tenant/bin/Debug/netcoreapp2.2
124-
/2-WebApp-graph-user/2-3-Multi-Tenant/obj

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ 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
Global
1118
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1219
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +28,10 @@ Global
2128
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Debug|Any CPU.Build.0 = Debug|Any CPU
2229
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.ActiveCfg = Release|Any CPU
2330
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.Build.0 = Release|Any CPU
31+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
33+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
34+
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.Build.0 = Release|Any CPU
2435
EndGlobalSection
2536
GlobalSection(SolutionProperties) = preSolution
2637
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
@@ -19,7 +19,7 @@
1919

2020
<ItemGroup>
2121
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="3.0.0" />
22-
<PackageReference Include="Microsoft.Graph" Version="1.14.0" />
22+
<PackageReference Include="Microsoft.Graph" Version="1.21.0" />
2323
</ItemGroup>
2424

2525
<ItemGroup>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.AspNetCore.Authentication.JwtBearer;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using Xunit;
6+
using System.Linq;
7+
8+
namespace Microsoft.Identity.Web.Test
9+
{
10+
public class WebApiServiceCollectionExtensionsTests
11+
{
12+
[Fact]
13+
public void TestAuthority()
14+
{
15+
// Arrange
16+
JwtBearerOptions options = new JwtBearerOptions();
17+
18+
// Act and Assert
19+
options.Authority = "https://login.onmicrosoft.com/common";
20+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
21+
Assert.Equal("https://login.onmicrosoft.com/common/v2.0", options.Authority);
22+
23+
options.Authority = "https://login.onmicrosoft.com/common/";
24+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
25+
Assert.Equal("https://login.onmicrosoft.com/common/v2.0", options.Authority);
26+
27+
options.Authority = "https://login.onmicrosoft.com/common/v2.0";
28+
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
29+
Assert.Equal("https://login.onmicrosoft.com/common/v2.0", options.Authority);
30+
}
31+
32+
[Fact]
33+
public void TestAudience()
34+
{
35+
JwtBearerOptions options = new JwtBearerOptions();
36+
37+
// Act and Assert
38+
options.Audience = "https://localhost";
39+
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
40+
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1);
41+
Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "https://localhost");
42+
43+
options.Audience = "api://1EE5A092-0DFD-42B6-88E5-C517C0141321";
44+
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
45+
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1);
46+
Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "api://1EE5A092-0DFD-42B6-88E5-C517C0141321");
47+
48+
options.Audience = "1EE5A092-0DFD-42B6-88E5-C517C0141321";
49+
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
50+
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 2);
51+
Assert.Contains("api://1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences);
52+
Assert.Contains("1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences);
53+
54+
}
55+
}
56+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
</PropertyGroup>
5454

5555
<ItemGroup>
56-
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="3.1.1" />
57-
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureADB2C.UI" Version="3.1.1" />
56+
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="3.0.0" />
57+
<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureADB2C.UI" Version="3.0.0" />
5858
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
5959
<PackageReference Include="Microsoft.Identity.Client" Version="4.8.1" />
6060
</ItemGroup>

Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,17 @@ public static IServiceCollection AddProtectedWebApi(
4747
services.AddHttpContextAccessor();
4848

4949
// Change the authentication configuration to accommodate the Microsoft identity platform endpoint (v2.0).
50-
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
50+
services.Configure(AzureADDefaults.JwtBearerAuthenticationScheme, (Action<JwtBearerOptions>)(options =>
5151
{
5252
// Reinitialize the options as this has changed to JwtBearerOptions to pick configuration values for attributes unique to JwtBearerOptions
5353
configuration.Bind(configSectionName, options);
5454

5555
// This is an Microsoft identity platform Web API
56-
var authority = options.Authority.Trim().TrimEnd('/');
57-
if (!authority.EndsWith("v2.0"))
58-
authority += "/v2.0";
59-
options.Authority = authority;
60-
61-
// The valid audience could be given as Client Id or as Uri. If it does not start with 'api://', this variant is added to the list of valid audiences.
62-
var validAudiences = new List<string> { options.Audience };
63-
if (!options.Audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase))
64-
validAudiences.Add($"api://{options.Audience}");
56+
EnsureAuthorityIsV2_0(options);
6557

66-
options.TokenValidationParameters.ValidAudiences = validAudiences;
58+
// The valid audience could be given as Client Id or as Uri.
59+
// If it does not start with 'api://', this variant is added to the list of valid audiences.
60+
EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
6761

6862
// Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
6963
// we inject our own multi-tenant validation logic (which even accepts both v1.0 and v2.0 tokens)
@@ -80,23 +74,23 @@ public static IServiceCollection AddProtectedWebApi(
8074
options.Events = new JwtBearerEvents();
8175

8276
options.Events.OnTokenValidated = async context =>
83-
{
84-
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
85-
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
86-
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
87-
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
88-
{
89-
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
90-
}
91-
92-
await Task.FromResult(0);
93-
};
77+
{
78+
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
79+
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
80+
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
81+
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
82+
{
83+
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
84+
}
85+
86+
await Task.FromResult(0);
87+
};
9488

9589
if (subscribeToJwtBearerMiddlewareDiagnosticsEvents)
9690
{
9791
options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
9892
}
99-
});
93+
}));
10094

10195
return services;
10296
}
@@ -130,5 +124,37 @@ public static IServiceCollection AddProtectedApiCallsWebApis(
130124

131125
return services;
132126
}
127+
128+
/// <summary>
129+
/// Ensures that the authority is a v2.0 authority
130+
/// </summary>
131+
/// <param name="options">Jwt bearer options read from the config file
132+
/// or set by the developper, for which we want to ensure the authority
133+
/// is a v2.0 authority</param>
134+
internal static void EnsureAuthorityIsV2_0(JwtBearerOptions options)
135+
{
136+
var authority = options.Authority.Trim().TrimEnd('/');
137+
if (!authority.EndsWith("v2.0"))
138+
authority += "/v2.0";
139+
options.Authority = authority;
140+
}
141+
142+
143+
/// <summary>
144+
/// Ensure that if the audience is a GUID, api://{audience} is also added
145+
/// as a valid audience (this is the default App ID URL in the app registration
146+
/// portal)
147+
/// </summary>
148+
/// <param name="options">Jwt bearer options for which to ensure that
149+
/// api://GUID is a valid audience</param>
150+
internal static void EnsureValidAudiencesContainsApiGuidIfGuidProvided(JwtBearerOptions options)
151+
{
152+
var validAudiences = new List<string> { options.Audience };
153+
if (!options.Audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase)
154+
&& Guid.TryParse(options.Audience, out _))
155+
validAudiences.Add($"api://{options.Audience}");
156+
157+
options.TokenValidationParameters.ValidAudiences = validAudiences;
158+
}
133159
}
134160
}

0 commit comments

Comments
 (0)