Skip to content

Commit

Permalink
Hmac authorization provider (#7)
Browse files Browse the repository at this point in the history
* implement authorization provider for HMAC

* update nuget packages

* update README, use registered IBasicAuthorizationProvider from serivces instead of newing up an instance
  • Loading branch information
meinsiedler authored Nov 2, 2020
1 parent 4721dbd commit fbacdc4
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 88 deletions.
114 changes: 59 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,67 +20,69 @@ Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/ap

Usage:

1. Get your HMAC authenticated clients, for example from the `appsettings.json` file. For HMAC authentication, an `AppId` and an `ApiKey` is required for each client which should get access.
1. Register an implementation of `IHmacAuthorizationProvider` in `Startup.cs`. Either use the built-in `MemoryHmacAuthenticationProvider` for in-memory HMAC app configuration or implement your own `IHmacAuthorizationProvider` to provide HMAC apps.

```csharp
services.AddTransient<IHmacAuthorizationProvider>(_ => new MemoryHmacAuthenticationProvider(hmacAuthenticatedApps));
```

```csharp
var hmacAuthenticatedApps = this.Configuration
.GetSection("Authentication")
.GetSection("HmacAuthenticatedApps")
.Get<HmacAuthenticationClientConfiguration[]>()
.ToDictionary(e => e.AppId, e => e.ApiKey);
```
2. When using the built-in `MemoryHmacAuthenticationProvider`, get your HMAC authenticated clients, for example from the `appsettings.json` file. For HMAC authentication, an `AppId` and an `ApiKey` is required for each client which should get access.

```json
{
"Authentication": {
"HmacAuthenticatedApps": [
{
"AppId": "<some-app-id>",
"ApiKey": "<some-api-key>"
}
]
}
}
```
```json
{
"Authentication": {
"HmacAuthenticatedApps": [
{
"AppId": "<some-app-id>",
"ApiKey": "<some-api-key>"
}
]
}
}
```

2. Enable HMAC authentication in `Startup.cs` in the `ConfigureServices` method:
```csharp
var hmacAuthenticatedApps = this.Configuration
.GetSection("Authentication")
.GetSection("HmacAuthenticatedApps")
.Get<HmacAuthenticationClientConfiguration[]>()
.ToDictionary(e => e.AppId, e => e.ApiKey);
```

```csharp
services
.AddAuthentication(o =>
{
o.DefaultScheme = HmacAuthenticationDefaults.AuthenticationScheme;
})
.AddHmacAuthentication(HmacAuthenticationDefaults.AuthenticationScheme, "HMAC Authentication", o =>
{
o.MaxRequestAgeInSeconds = HmacAuthenticationDefaults.MaxRequestAgeInSeconds;
o.HmacAuthenticatedApps = hmacAuthenticatedApps;
});
```
3. Enable HMAC authentication in `Startup.cs` in the `ConfigureServices` method. The `.AddHmacAuthentication(...)` method will use the configured `IHmacAuthorizationProvider` for resolving HMAC apps:

3. Add `MemoryCache` (from [Microsoft.Extensions.Caching.Memory](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/)) in `Startup.cs` in the `ConfigureServices` method.
```csharp
services
.AddAuthentication(o =>
{
o.DefaultScheme = HmacAuthenticationDefaults.AuthenticationScheme;
})
.AddHmacAuthentication(HmacAuthenticationDefaults.AuthenticationScheme, "HMAC Authentication", options => { });
```

4. Add `MemoryCache` (from [Microsoft.Extensions.Caching.Memory](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/)) in `Startup.cs` in the `ConfigureServices` method.
The `MemoryCache` is used by the HMAC `AuthenticationHandler` to determine replay attacks.

```csharp
services.AddMemoryCache();
```
```csharp
services.AddMemoryCache();
```

4. Enable authentication in `Startup.cs` in the `Configure` method:
5. Enable authentication in `Startup.cs` in the `Configure` method:

```csharp
app.UseAuthentication();
```
```csharp
app.UseAuthentication();
```

5. Optional: Specify HMAC as the authentication scheme for certain controllers:
6. Optional: Specify HMAC as the authentication scheme for certain controllers:

```csharp
[Authorize(AuthenticationSchemes = HmacAuthenticationDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
public class HomeController : Controller
{
// ...
}
```
```csharp
[Authorize(AuthenticationSchemes = HmacAuthenticationDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
public class HomeController : Controller
{
// ...
}
```

### softaware.Authentication.Hmac.Client

Expand Down Expand Up @@ -141,16 +143,18 @@ Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/ap
Enable Basic authentication in `Startup.cs` in the `ConfigureServices` method:

```csharp
services.AddAuthentication(o =>
services.AddTransient<IBasicAuthenticationProvider>(_ => new MemoryBasicAuthenticationProvider(authenticatedApps));

services
.AddAuthentication(o =>
{
o.DefaultScheme = BasicAuthenticationDefaults.AuthenticationScheme;
})
.AddBasicAuthentication(BasicAuthenticationDefaults.AuthenticationScheme, o =>
{
o.AuthorizationProvider = new MemoryBasicAuthenticationProvider(authenticatedApps);
});
.AddBasicAuthentication();
```

If you want to validate usernames and passwords from the basic authentication header more sophisticated than the built-in `MemoryBasicAuthenticationProvider`, just implement and register your own `IBasicAuthenticationProvider`.

### softaware.Authentication.Basic.Client

Provides a `DelegatingHandler` for adding an HMAC authorization header to HTTP requests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddTransient(_ => this.basicAuthorizationProvider);

services.AddAuthentication(o =>
{
o.DefaultScheme = BasicAuthenticationDefaults.AuthenticationScheme;
})
.AddBasicAuthentication(BasicAuthenticationDefaults.AuthenticationScheme, o =>
{
o.AuthorizationProvider = basicAuthorizationProvider;
});
.AddBasicAuthentication();
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
47 changes: 46 additions & 1 deletion softaware.Authentication.Hmac.AspNetCore.Test/MiddlewareTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using softaware.Authentication.Hmac.AuthorizationProvider;
using softaware.Authentication.Hmac.Client;
using Xunit;

Expand All @@ -22,6 +23,32 @@ public Task Request_Authorized()
HttpStatusCode.OK);
}

[Fact]
public async Task Request_WithDeprecatedHmacAuthorizedAppsOption_Authorized()
{
using (var client = this.GetHttpClientWithHmacAutenticatedAppsOption(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w="))
{
var response = await client.GetAsync("api/test");
Assert.True(response.StatusCode == HttpStatusCode.OK);
}
}

[Fact]
public async Task Request_WithDefaultAuthorizationProvider_Authorized()
{
using (var client = this.GetHttpClientWithDefaultAuthorizationProvider(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w="))
{
var response = await client.GetAsync("api/test");
Assert.True(response.StatusCode == HttpStatusCode.OK);
}
}

[Theory]
[InlineData("appId", "YXJld3JzZHJkc2FhcndlZQ==")]
[InlineData("wrongAppId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=")]
Expand Down Expand Up @@ -104,7 +131,25 @@ private async Task<HttpResponseMessage> TestRequestAsync(

private HttpClient GetHttpClient(IDictionary<string, string> hmacAuthenticatedApps, string appId, string apiKey)
{
var factory = new TestWebApplicationFactory(hmacAuthenticatedApps);
var factory = new TestWebApplicationFactory(o =>
{
o.AuthorizationProvider = new MemoryHmacAuthenticationProvider(hmacAuthenticatedApps);
});
return factory.CreateDefaultClient(new ApiKeyDelegatingHandler(appId, apiKey));
}

private HttpClient GetHttpClientWithHmacAutenticatedAppsOption(IDictionary<string, string> hmacAuthenticatedApps, string appId, string apiKey)
{
var factory = new TestWebApplicationFactory(o =>
{
o.HmacAuthenticatedApps = hmacAuthenticatedApps;
});
return factory.CreateDefaultClient(new ApiKeyDelegatingHandler(appId, apiKey));
}

private HttpClient GetHttpClientWithDefaultAuthorizationProvider(IDictionary<string, string> hmacAuthenticatedApps, string appId, string apiKey)
{
var factory = new TestWebApplicationFactoryWithDefaultAuthorizationProvider(hmacAuthenticatedApps);
return factory.CreateDefaultClient(new ApiKeyDelegatingHandler(appId, apiKey));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -7,11 +7,11 @@ namespace softaware.Authentication.Hmac.AspNetCore.Test
{
public class TestWebApplicationFactory : WebApplicationFactory<TestStartup>
{
private readonly IDictionary<string, string> hmacAuthenticatedApps;
private readonly Action<HmacAuthenticationSchemeOptions> configureOptions;

public TestWebApplicationFactory(IDictionary<string, string> hmacAuthenticatedApps)
public TestWebApplicationFactory(Action<HmacAuthenticationSchemeOptions> configureOptions)
{
this.hmacAuthenticatedApps = hmacAuthenticatedApps;
this.configureOptions = configureOptions;
}

protected override IWebHostBuilder CreateWebHostBuilder()
Expand All @@ -29,12 +29,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
})
.AddHmacAuthentication(
HmacAuthenticationDefaults.AuthenticationScheme,
HmacAuthenticationDefaults.AuthenticationType,
o =>
{
o.MaxRequestAgeInSeconds = HmacAuthenticationDefaults.MaxRequestAgeInSeconds;
o.HmacAuthenticatedApps = this.hmacAuthenticatedApps;
});
HmacAuthenticationDefaults.AuthenticationType,
this.configureOptions);
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using softaware.Authentication.Hmac.AuthorizationProvider;

namespace softaware.Authentication.Hmac.AspNetCore.Test
{
public class TestWebApplicationFactoryWithDefaultAuthorizationProvider : WebApplicationFactory<TestStartup>
{
private readonly IDictionary<string, string> hmacAuthenticatedApps;

public TestWebApplicationFactoryWithDefaultAuthorizationProvider(IDictionary<string, string> hmacAuthenticatedApps)
{
this.hmacAuthenticatedApps = hmacAuthenticatedApps ?? throw new ArgumentNullException(nameof(hmacAuthenticatedApps));
}

protected override IWebHostBuilder CreateWebHostBuilder()
{
return new WebHostBuilder().UseStartup<TestStartup>();
}

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddTransient<IHmacAuthorizationProvider>(sp => new MemoryHmacAuthenticationProvider(this.hmacAuthenticatedApps));

services.AddAuthentication(o =>
{
o.DefaultScheme = HmacAuthenticationDefaults.AuthenticationScheme;
})
.AddHmacAuthentication();
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand All @@ -20,6 +20,7 @@
<ItemGroup>
<ProjectReference Include="..\softaware.Authentication.Hmac.AspNetCore\softaware.Authentication.Hmac.AspNetCore.csproj" />
<ProjectReference Include="..\softaware.Authentication.Hmac.Client\softaware.Authentication.Hmac.Client.csproj" />
<ProjectReference Include="..\softaware.Authentication.Hmac\softaware.Authentication.Hmac.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using softaware.Authentication.Hmac.AuthorizationProvider;

namespace softaware.Authentication.Hmac.AspNetCore
{
Expand All @@ -9,7 +11,22 @@ public class HmacAuthenticationSchemeOptions : AuthenticationSchemeOptions

public string AuthenticationScheme { get; set; }

public IDictionary<string, string> HmacAuthenticatedApps { get; set; } = new Dictionary<string, string>();
private IDictionary<string, string> hmacAuthenticatedApps = new Dictionary<string, string>();

[Obsolete("Please use the MemoryHmacAuthenticationProvider for configuring the HMAC apps in-memory. This property will be removed in future versions of this package.", error: false)]
public IDictionary<string, string> HmacAuthenticatedApps
{
get { return this.hmacAuthenticatedApps; }
set
{
this.hmacAuthenticatedApps = value;

// temporary backwards compatible solution as long as we support HmacAuthenticatedApps.
this.AuthorizationProvider = new MemoryHmacAuthenticationProvider(this.hmacAuthenticatedApps);
}
}

public IHmacAuthorizationProvider AuthorizationProvider { get; set; }

public HmacAuthenticationSchemeOptions()
{
Expand Down
Loading

0 comments on commit fbacdc4

Please sign in to comment.