Skip to content

Commit 0ab28e7

Browse files
authored
Adding unit tests to #278 (#284)
* Adding unit tests to #278 and reverting the UI package upgrade as 3.1.1 has a bug * Undoing what the refactoring tools in VS have changed * Updating the AzureADxx.UI packages * Adressing PR comments
1 parent 21474e3 commit 0ab28e7

File tree

5 files changed

+128
-26
lines changed

5 files changed

+128
-26
lines changed

.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,7 @@
114114
/4-WebApp-your-API/Client/obj
115115
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin
116116
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/obj
117-
/Microsoft.Identity.Web.Test/bin/Release/netcoreapp2.2
118117
/Microsoft.Identity.Web.Test/obj
119118
/4-WebApp-your-API/4-2-B2C/.vs
120119
/4-WebApp-your-API/4-2-B2C/Client/obj
121120
/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
@@ -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>
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/WebApiServiceCollectionExtensions.cs

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,11 @@ public static IServiceCollection AddProtectedWebApi(
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;
56+
EnsureAuthorityIsV2_0(options);
6057

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}");
65-
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,17 +74,17 @@ 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
{
@@ -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)