Skip to content
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

Add support for AzureAD authentication for REST admin interface #637

Merged
merged 9 commits into from
Oct 20, 2021
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</PropertyGroup>

<PropertyGroup>
<VersionPrefix>1.4.23</VersionPrefix>
<VersionPrefix>1.4.24</VersionPrefix>
<PackageReleaseNotes>See CHANGELOG.md</PackageReleaseNotes>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https:/WireMock-Net/WireMock.Net</PackageProjectUrl>
Expand Down
8 changes: 8 additions & 0 deletions examples/WireMock.Net.Console.Net452.Classic/MainApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,17 @@ public static void Run()
System.Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));

server.SetBasicAuthentication("a", "b");
//server.SetAzureADAuthentication("6c2a4722-f3b9-4970-b8fc-fac41e29stef", "8587fde1-7824-42c7-8592-faf92b04stef");

// server.AllowPartialMapping();

server.Given(Request.Create().WithPath("/mypath").UsingPost())
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson("{{JsonPath.SelectToken request.body \"..name\"}}")
.WithTransformer()
);

server
.Given(Request.Create().WithPath(p => p.Contains("x")).UsingGet())
.AtPriority(4)
Expand Down
13 changes: 10 additions & 3 deletions src/WireMock.Net.Abstractions/Server/IWireMockServer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using JetBrains.Annotations;
Expand Down Expand Up @@ -104,9 +104,9 @@ public interface IWireMockServer : IDisposable
void ReadStaticMappings([CanBeNull] string folder = null);

/// <summary>
/// Removes the basic authentication.
/// Removes the authentication.
/// </summary>
void RemoveBasicAuthentication();
void RemoveAuthentication();

/// <summary>
/// Resets LogEntries and Mappings.
Expand Down Expand Up @@ -134,6 +134,13 @@ public interface IWireMockServer : IDisposable
/// <param name="folder">The optional folder. If not defined, use {CurrentFolder}/__admin/mappings</param>
void SaveStaticMappings([CanBeNull] string folder = null);

/// <summary>
/// Sets the basic authentication.
/// </summary>
/// <param name="tenant">The Tenant.</param>
/// <param name="audience">The Audience or Resource.</param>
void SetAzureADAuthentication([NotNull] string tenant, [NotNull] string audience);

/// <summary>
/// Sets the basic authentication.
/// </summary>
Expand Down
71 changes: 71 additions & 0 deletions src/WireMock.Net/Authentication/AzureADAuthenticationMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#if !NETSTANDARD1_3
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Text.RegularExpressions;
using AnyOfTypes;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using WireMock.Matchers;
using WireMock.Models;

namespace WireMock.Authentication
{
/// <summary>
/// https://www.c-sharpcorner.com/article/how-to-validate-azure-ad-token-using-console-application/
/// https://stackoverflow.com/questions/38684865/validation-of-an-azure-ad-bearer-token-in-a-console-application
/// </summary>
internal class AzureADAuthenticationMatcher : IStringMatcher
{
private const string BearerPrefix = "Bearer ";

private readonly string _audience;
private readonly string _stsDiscoveryEndpoint;

public AzureADAuthenticationMatcher(string tenant, string audience)
{
_audience = audience;
_stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", tenant);
}

public string Name => nameof(AzureADAuthenticationMatcher);

public MatchBehaviour MatchBehaviour => MatchBehaviour.AcceptOnMatch;

public bool ThrowException => false;

public AnyOf<string, StringPattern>[] GetPatterns()
{
return new AnyOf<string, StringPattern>[0];
}

public double IsMatch(string input)
{
var token = Regex.Replace(input, BearerPrefix, string.Empty, RegexOptions.IgnoreCase);

try
{
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(_stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = configManager.GetConfigurationAsync().GetAwaiter().GetResult();

var validationParameters = new TokenValidationParameters
{
ValidAudience = _audience,
ValidIssuer = config.Issuer,
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = true
};

// Throws an Exception as the token is invalid (expired, invalid-formatted, etc.)
new JwtSecurityTokenHandler().ValidateToken(token, validationParameters, out var _);

return MatchScores.Perfect;
}
catch
{
return MatchScores.Mismatch;
}
}
}
}
#endif
20 changes: 20 additions & 0 deletions src/WireMock.Net/Authentication/BasicAuthenticationMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Text;
using WireMock.Matchers;

namespace WireMock.Authentication
{
internal class BasicAuthenticationMatcher : RegexMatcher
{
public BasicAuthenticationMatcher(string username, string password) : base(BuildPattern(username, password))
{
}

public override string Name => nameof(BasicAuthenticationMatcher);

private static string BuildPattern(string username, string password)
{
return "^(?i)BASIC " + Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password)) + "$";
}
}
}
4 changes: 2 additions & 2 deletions src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using WireMock.Handlers;
using WireMock.Logging;
Expand All @@ -19,7 +19,7 @@ internal interface IWireMockMiddlewareOptions

TimeSpan? RequestProcessingDelay { get; set; }

IStringMatcher AuthorizationMatcher { get; set; }
IStringMatcher AuthenticationMatcher { get; set; }

bool? AllowPartialMapping { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions src/WireMock.Net/Owin/WireMockMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ private async Task InvokeInternal(IContext ctx)

logRequest = targetMapping.LogMapping;

if (targetMapping.IsAdminInterface && _options.AuthorizationMatcher != null)
if (targetMapping.IsAdminInterface && _options.AuthenticationMatcher != null)
{
bool present = request.Headers.TryGetValue(HttpKnownHeaderNames.Authorization, out WireMockList<string> authorization);
if (!present || _options.AuthorizationMatcher.IsMatch(authorization.ToString()) < MatchScores.Perfect)
if (!present || _options.AuthenticationMatcher.IsMatch(authorization.ToString()) < MatchScores.Perfect)
{
_options.Logger.Error("HttpStatusCode set to 401");
response = ResponseMessageBuilder.Create(null, 401);
Expand Down
2 changes: 1 addition & 1 deletion src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions

public TimeSpan? RequestProcessingDelay { get; set; }

public IStringMatcher AuthorizationMatcher { get; set; }
public IStringMatcher AuthenticationMatcher { get; set; }

public bool? AllowPartialMapping { get; set; }

Expand Down
31 changes: 25 additions & 6 deletions src/WireMock.Net/Server/WireMockServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JetBrains.Annotations;
using Newtonsoft.Json;
using WireMock.Admin.Mappings;
using WireMock.Authentication;
using WireMock.Exceptions;
using WireMock.Handlers;
using WireMock.Logging;
Expand Down Expand Up @@ -302,6 +303,11 @@ protected WireMockServer(IWireMockServerSettings settings)
SetBasicAuthentication(settings.AdminUsername, settings.AdminPassword);
}

if (!string.IsNullOrEmpty(settings.AdminAzureADTenant) && !string.IsNullOrEmpty(settings.AdminAzureADAudience))
{
SetAzureADAuthentication(settings.AdminAzureADTenant, settings.AdminAzureADAudience);
}

InitAdmin();
}

Expand Down Expand Up @@ -404,22 +410,35 @@ public void AllowPartialMapping(bool allow = true)
_options.AllowPartialMapping = allow;
}

/// <inheritdoc cref="IWireMockServer.SetBasicAuthentication" />
/// <inheritdoc cref="IWireMockServer.SetAzureADAuthentication(string, string)" />
[PublicAPI]
public void SetAzureADAuthentication([NotNull] string tenant, [NotNull] string audience)
{
Check.NotNull(tenant, nameof(tenant));
Check.NotNull(audience, nameof(audience));

#if NETSTANDARD1_3
throw new NotSupportedException("AzureADAuthentication is not supported for NETStandard 1.3");
#else
_options.AuthenticationMatcher = new AzureADAuthenticationMatcher(tenant, audience);
#endif
}

/// <inheritdoc cref="IWireMockServer.SetBasicAuthentication(string, string)" />
[PublicAPI]
public void SetBasicAuthentication([NotNull] string username, [NotNull] string password)
{
Check.NotNull(username, nameof(username));
Check.NotNull(password, nameof(password));

string authorization = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));
_options.AuthorizationMatcher = new RegexMatcher(MatchBehaviour.AcceptOnMatch, "^(?i)BASIC " + authorization + "$");
_options.AuthenticationMatcher = new BasicAuthenticationMatcher(username, password);
}

/// <inheritdoc cref="IWireMockServer.RemoveBasicAuthentication" />
/// <inheritdoc cref="IWireMockServer.RemoveAuthentication" />
[PublicAPI]
public void RemoveBasicAuthentication()
public void RemoveAuthentication()
{
_options.AuthorizationMatcher = null;
_options.AuthenticationMatcher = null;
}

/// <inheritdoc cref="IWireMockServer.SetMaxRequestLogCount" />
Expand Down
14 changes: 13 additions & 1 deletion src/WireMock.Net/Settings/IWireMockServerSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using HandlebarsDotNet;
using JetBrains.Annotations;
using WireMock.Handlers;
Expand Down Expand Up @@ -88,6 +88,18 @@ public interface IWireMockServerSettings
[PublicAPI]
string AdminPassword { get; set; }

/// <summary>
/// The AzureAD Tenant needed for __admin access.
/// </summary>
[PublicAPI]
string AdminAzureADTenant { get; set; }

/// <summary>
/// The AzureAD Audience / Resource for __admin access.
/// </summary>
[PublicAPI]
string AdminAzureADAudience { get; set; }

/// <summary>
/// The RequestLog expiration in hours (optional).
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion src/WireMock.Net/Settings/WireMockServerSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using HandlebarsDotNet;
using JetBrains.Annotations;
using Newtonsoft.Json;
Expand Down Expand Up @@ -64,6 +64,14 @@ public class WireMockServerSettings : IWireMockServerSettings
[PublicAPI]
public string AdminPassword { get; set; }

/// <inheritdoc cref="IWireMockServerSettings.AdminAzureADTenant"/>
[PublicAPI]
public string AdminAzureADTenant { get; set; }

/// <inheritdoc cref="IWireMockServerSettings.AdminAzureADAudience"/>
[PublicAPI]
public string AdminAzureADAudience { get; set; }

/// <inheritdoc cref="IWireMockServerSettings.RequestLogExpirationDuration"/>
[PublicAPI]
public int? RequestLogExpirationDuration { get; set; }
Expand Down
4 changes: 3 additions & 1 deletion src/WireMock.Net/Settings/WireMockServerSettingsParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using JetBrains.Annotations;
using JetBrains.Annotations;
using WireMock.Logging;
using WireMock.Validation;

Expand Down Expand Up @@ -39,6 +39,8 @@ public static bool TryParseArguments([NotNull] string[] args, out IWireMockServe
WatchStaticMappingsInSubdirectories = parser.GetBoolValue("WatchStaticMappingsInSubdirectories"),
AdminUsername = parser.GetStringValue("AdminUsername"),
AdminPassword = parser.GetStringValue("AdminPassword"),
AdminAzureADTenant = parser.GetStringValue(nameof(IWireMockServerSettings.AdminAzureADTenant)),
AdminAzureADAudience = parser.GetStringValue(nameof(IWireMockServerSettings.AdminAzureADAudience)),
MaxRequestLogCount = parser.GetIntValue("MaxRequestLogCount"),
RequestLogExpirationDuration = parser.GetIntValue("RequestLogExpirationDuration"),
AllowCSharpCodeMatcher = parser.GetBoolValue("AllowCSharpCodeMatcher"),
Expand Down
30 changes: 16 additions & 14 deletions src/WireMock.Net/WireMock.Net.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Lightweight Http Mocking Server for .Net, inspired by WireMock from the Java landscape.</Description>
<AssemblyTitle>WireMock.Net</AssemblyTitle>
Expand Down Expand Up @@ -50,16 +50,17 @@
<DefineConstants>USE_ASPNETCORE;NET46</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="SimMetrics.Net" Version="1.0.5" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.12" />
<PackageReference Include="RandomDataGenerator.Net" Version="1.0.12" />
<PackageReference Include="JmesPath.Net" Version="1.0.125" />
<PackageReference Include="AnyOf" Version="0.2.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="SimMetrics.Net" Version="1.0.5" />
<!--<PackageReference Include="Stef.Validation" Version="0.0.3" />-->
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.12" />
<PackageReference Include="RandomDataGenerator.Net" Version="1.0.12" />
<PackageReference Include="JmesPath.Net" Version="1.0.125" />
<PackageReference Include="AnyOf" Version="0.2.0" />
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' == 'Debug - Sonar'">
<PackageReference Include="SonarAnalyzer.CSharp" Version="7.8.0.7320">
Expand All @@ -68,9 +69,10 @@
</PackageReference>
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.3' ">
<PackageReference Include="XPath2.Extensions" Version="1.1.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard1.3' ">
<PackageReference Include="XPath2.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.12.2" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net451' or '$(TargetFramework)' == 'net452' ">
<!-- Required for WebRequestHandler -->
Expand Down
8 changes: 4 additions & 4 deletions test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public async Task WireMockMiddleware_Invoke_IsAdminInterface_EmptyHeaders_401()
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary<string, string[]>());
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);

_optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher());
_optionsMock.SetupGet(o => o.AuthenticationMatcher).Returns(new ExactMatcher());
_mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true);

var result = new MappingMatcherResult { Mapping = _mappingMock.Object };
Expand All @@ -119,7 +119,7 @@ public async Task WireMockMiddleware_Invoke_IsAdminInterface_MissingHeader_401()
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary<string, string[]> { { "h", new[] { "x" } } });
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);

_optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher());
_optionsMock.SetupGet(o => o.AuthenticationMatcher).Returns(new ExactMatcher());
_mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true);

var result = new MappingMatcherResult { Mapping = _mappingMock.Object };
Expand Down Expand Up @@ -152,7 +152,7 @@ public async Task WireMockMiddleware_Invoke_Mapping_Has_ProxyAndRecordSettings_A
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary<string, string[]>());
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);

_optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher());
_optionsMock.SetupGet(o => o.AuthenticationMatcher).Returns(new ExactMatcher());

var fileSystemHandlerMock = new Mock<IFileSystemHandler>();
fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m");
Expand Down Expand Up @@ -201,7 +201,7 @@ public async Task WireMockMiddleware_Invoke_Mapping_Has_ProxyAndRecordSettings_A
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary<string, string[]>());
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);

_optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher());
_optionsMock.SetupGet(o => o.AuthenticationMatcher).Returns(new ExactMatcher());

var fileSystemHandlerMock = new Mock<IFileSystemHandler>();
fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m");
Expand Down
Loading