Skip to content

Adding unit tests to #278 #284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,7 @@
/4-WebApp-your-API/Client/obj
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin
/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/obj
/Microsoft.Identity.Web.Test/bin/Release/netcoreapp2.2
/Microsoft.Identity.Web.Test/obj
/4-WebApp-your-API/4-2-B2C/.vs
/4-WebApp-your-API/4-2-B2C/Client/obj
/4-WebApp-your-API/4-2-B2C/TodoListService/obj
/2-WebApp-graph-user/2-3-Multi-Tenant/.vs/WebApp-OpenIDConnect-DotNet
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was already present in the .gitignore

/2-WebApp-graph-user/2-3-Multi-Tenant/bin/Debug/netcoreapp2.2
/2-WebApp-graph-user/2-3-Multi-Tenant/obj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp-OpenIDConnect-DotNet
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web", "..\..\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj", "{E0CEF26A-6CE6-4505-851B-6580D5564752}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CD0BB564-6C1E-4FCE-B9AB-7C637FDEE569}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test", "..\..\Microsoft.Identity.Web.Test\Microsoft.Identity.Web.Test.csproj", "{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the unit tests assembly to the solution

EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +28,10 @@ Global
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.Build.0 = Release|Any CPU
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Graph" Version="1.14.0" />
<PackageReference Include="Microsoft.Graph" Version="1.21.0" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating to the latest Graph SDK

</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
using System.Linq;

namespace Microsoft.Identity.Web.Test
{
public class WebApiServiceCollectionExtensionsTests
{
[Fact]
public void TestAuthority()
{
// Arrange
JwtBearerOptions options = new JwtBearerOptions();

// Act and Assert
options.Authority = "https://login.microsoftonline.com/common";
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);

options.Authority = "https://login.microsoftonline.us/organizations";
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
Assert.Equal("https://login.microsoftonline.us/organizations/v2.0", options.Authority);

options.Authority = "https://login.microsoftonline.com/common/";
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);

options.Authority = "https://login.microsoftonline.com/common/v2.0";
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);


options.Authority = "https://login.microsoftonline.com/common/v2.0";
WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options);
Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority);

}

[Fact]
public void TestAudience()
{
JwtBearerOptions options = new JwtBearerOptions();

// Act and Assert
options.Audience = "https://localhost";
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1);
Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "https://localhost");

options.Audience = "api://1EE5A092-0DFD-42B6-88E5-C517C0141321";
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1);
Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "api://1EE5A092-0DFD-42B6-88E5-C517C0141321");

options.Audience = "1EE5A092-0DFD-42B6-88E5-C517C0141321";
WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 2);
Assert.Contains("api://1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences);
Assert.Contains("1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences);

}
}
}
68 changes: 47 additions & 21 deletions Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,11 @@ public static IServiceCollection AddProtectedWebApi(
configuration.Bind(configSectionName, options);

// This is an Microsoft identity platform Web API
var authority = options.Authority.Trim().TrimEnd('/');
if (!authority.EndsWith("v2.0"))
authority += "/v2.0";
options.Authority = authority;
EnsureAuthorityIsV2_0(options);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactoring for testability


// 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.
var validAudiences = new List<string> { options.Audience };
if (!options.Audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase))
validAudiences.Add($"api://{options.Audience}");

options.TokenValidationParameters.ValidAudiences = validAudiences;
// 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.
EnsureValidAudiencesContainsApiGuidIfGuidProvided(options);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactoring for testability


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

options.Events.OnTokenValidated = async context =>
{
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
{
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
}

await Task.FromResult(0);
};
{
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
{
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
}

await Task.FromResult(0);
};

if (subscribeToJwtBearerMiddlewareDiagnosticsEvents)
{
Expand Down Expand Up @@ -130,5 +124,37 @@ public static IServiceCollection AddProtectedApiCallsWebApis(

return services;
}

/// <summary>
/// Ensures that the authority is a v2.0 authority
/// </summary>
/// <param name="options">Jwt bearer options read from the config file
/// or set by the developper, for which we want to ensure the authority
/// is a v2.0 authority</param>
internal static void EnsureAuthorityIsV2_0(JwtBearerOptions options)
{
var authority = options.Authority.Trim().TrimEnd('/');
if (!authority.EndsWith("v2.0"))
authority += "/v2.0";
options.Authority = authority;
}


/// <summary>
/// Ensure that if the audience is a GUID, api://{audience} is also added
/// as a valid audience (this is the default App ID URL in the app registration
/// portal)
/// </summary>
/// <param name="options">Jwt bearer options for which to ensure that
/// api://GUID is a valid audience</param>
internal static void EnsureValidAudiencesContainsApiGuidIfGuidProvided(JwtBearerOptions options)
{
var validAudiences = new List<string> { options.Audience };
if (!options.Audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase)
&& Guid.TryParse(options.Audience, out _))
validAudiences.Add($"api://{options.Audience}");

options.TokenValidationParameters.ValidAudiences = validAudiences;
}
}
}