Skip to content

Commit 4f536bb

Browse files
authored
SAS Token Authentication (#23)
* add suport for SAS token auth * add unit tests * remove unused PostConfigureOptions * change SasTokenType to QueryParameterHandling, add known parameters to SAS token, increase NuGet version * add option to automatically append specified query parameters to sas url * increase NuGet version * add option for NameIdentifier claim type as query parameter * improvement: add sp query parameter only when allowing additional query parameters
1 parent 8d416c2 commit 4f536bb

20 files changed

+906
-8
lines changed

README.md

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
<!-- TOC -->
44

5+
- [softaware.Authentication](#softawareauthentication)
56
- [softaware.Authentication.Hmac](#softawareauthenticationhmac)
6-
- [softaware.Authentication.Hmac.AspNetCore](#softawareauthenticationhmacaspnetcore)
7-
- [softaware.Authentication.Hmac.Client](#softawareauthenticationhmacclient)
8-
- [Generate HMAC AppId and ApiKey](#generate-hmac-appid-and-apikey)
7+
- [softaware.Authentication.Hmac.AspNetCore](#softawareauthenticationhmacaspnetcore)
8+
- [softaware.Authentication.Hmac.Client](#softawareauthenticationhmacclient)
9+
- [Generate HMAC AppId and ApiKey](#generate-hmac-appid-and-apikey)
910
- [softaware.Authentication.Basic](#softawareauthenticationbasic)
10-
- [softaware.Authentication.Basic.AspNetCore](#softawareauthenticationbasicaspnetcore)
11-
- [softaware.Authentication.Basic.Client](#softawareauthenticationbasicclient)
11+
- [softaware.Authentication.Basic.AspNetCore](#softawareauthenticationbasicaspnetcore)
12+
- [softaware.Authentication.Basic.Client](#softawareauthenticationbasicclient)
13+
- [softaware.Authentication.SasToken](#softawareauthenticationsastoken)
14+
- [softaware.Authentication.SasToken.AspNetCore](#softawareauthenticationsastokenaspnetcore)
1215

1316
<!-- /TOC -->
1417

@@ -18,7 +21,7 @@
1821

1922
[![Nuget](https://img.shields.io/nuget/v/softaware.Authentication.Hmac.AspNetCore)](https://www.nuget.org/packages/softaware.Authentication.Hmac.AspNetCore)
2023

21-
Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1?view=aspnetcore-2.1) which supports [HMAC](https://en.wikipedia.org/wiki/HMAC) authentication in an ASP.NET Core project.
24+
Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) which supports [HMAC](https://en.wikipedia.org/wiki/HMAC) authentication in an ASP.NET Core project.
2225

2326
Usage:
2427

@@ -99,7 +102,7 @@ Make sure, that you don't create new `HttpClient` instances for every request (s
99102
new HttpClient(new ApiKeyDelegatingHandler(appId, apiKey));
100103
```
101104

102-
Or in case your WebAPI client is another ASP.NET WebAPI (>= [ASP.NET Core 2.1](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient?view=aspnetcore-2.1)), register your `HttpClient` in the `Startup.cs` for example as follows:
105+
Or in case your WebAPI client is another ASP.NET WebAPI (>= [ASP.NET Core 2.1](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient)), register your `HttpClient` in the `Startup.cs` for example as follows:
103106
104107
```csharp
105108
services.AddTransient(sp => new ApiKeyDelegatingHandler(appId, apiKey));
@@ -144,7 +147,7 @@ public class Program
144147

145148
[![Nuget](https://img.shields.io/nuget/v/softaware.Authentication.Basic.AspNetCore)](https://www.nuget.org/packages/softaware.Authentication.Basic.AspNetCore)
146149
147-
Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1?view=aspnetcore-2.1) which supports [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication) authentication in an ASP.NET Core project.
150+
Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) which supports [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication) authentication in an ASP.NET Core project.
148151
149152
Enable Basic authentication in `Startup.cs` in the `ConfigureServices` method:
150153

@@ -178,3 +181,33 @@ Make sure, that you don't create new `HttpClient` instances for every request (s
178181
```csharp
179182
new HttpClient(new BasicAuthenticationDelegatingHandler(username, password));
180183
```
184+
185+
## softaware.Authentication.SasToken
186+
187+
### softaware.Authentication.SasToken.AspNetCore
188+
189+
[![Nuget](https://img.shields.io/nuget/v/softaware.Authentication.SasToken.AspNetCore)](https://www.nuget.org/packages/softaware.Authentication.SasToken.AspNetCore)
190+
191+
Provides an [`AuthenticationHandler`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) which supports Shared Access Signature (SAS) authentication in an ASP.NET Core project.
192+
193+
Enable SAS authentication in `Startup.cs` in the `ConfigureServices` method:
194+
195+
```csharp
196+
services.AddTransient<IKeyProvider>(_ => new MemoryKeyProvider(key));
197+
198+
services
199+
.AddAuthentication(o =>
200+
{
201+
o.DefaultScheme = SasTokenAuthenticationDefaults.AuthenticationScheme;
202+
})
203+
.AddSasTokenAuthentication();
204+
```
205+
206+
If you want to retrieve the key for the shared access signature other than from the built-in `MemoryKeyProvider`, you can implement and register your own `IKeyProvider`.
207+
208+
Enable Authentication in `Startup.cs` in the `Configure` method:
209+
```csharp
210+
app.UseAuthentication();
211+
```
212+
213+
To generate an URL with a Shared Access Signature, inject the `SasTokenUrlGenerator` and call the `GenerateSasTokenQueryStringAsync(...)` method for getting the SAS query string or `GenerateSasTokenUriAsync(...)` method to receive the full SAS URI.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
using System.Net;
2+
using Microsoft.AspNetCore.WebUtilities;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Primitives;
5+
using softaware.Authentication.SasToken.AspNetCore.Test;
6+
using softaware.Authentication.SasToken.Generators;
7+
using softaware.Authentication.SasToken.KeyProvider;
8+
using softaware.Authentication.SasToken.Models;
9+
10+
namespace softaware.Authentication.Basic.AspNetCore.Test
11+
{
12+
public class MiddlewareTest
13+
{
14+
private TestWebApplicationFactory webAppFactory;
15+
private HttpClient httpClient;
16+
private SasTokenUrlGenerator urlGenerator;
17+
18+
public MiddlewareTest()
19+
{
20+
var keyProvider = new MemoryKeyProvider("key");
21+
this.webAppFactory = new TestWebApplicationFactory(keyProvider, nameIdentifierQueryParameter: null);
22+
23+
this.urlGenerator = this.webAppFactory.Services.GetRequiredService<SasTokenUrlGenerator>();
24+
this.httpClient = this.webAppFactory.CreateDefaultClient();
25+
}
26+
27+
[Fact]
28+
public async Task Request_Authorized()
29+
{
30+
var relativeUrl = "api/test" + await this.urlGenerator.GenerateSasTokenQueryStringAsync(
31+
"/api/test",
32+
DateTime.UtcNow,
33+
DateTime.UtcNow.AddMinutes(5),
34+
QueryParameterHandlingType.DenyAdditionalQueryParameters,
35+
CancellationToken.None);
36+
37+
await this.TestRequestAsync(
38+
relativeUrl,
39+
HttpStatusCode.NoContent);
40+
}
41+
42+
[Fact]
43+
public async Task Request_EndDateReached_NotAuthorized()
44+
{
45+
var relativeUrl = "api/test" + await this.urlGenerator.GenerateSasTokenQueryStringAsync(
46+
"/api/test",
47+
DateTime.UtcNow.AddMinutes(-10),
48+
DateTime.UtcNow.AddMinutes(-5),
49+
QueryParameterHandlingType.DenyAdditionalQueryParameters,
50+
CancellationToken.None);
51+
52+
await this.TestRequestAsync(
53+
relativeUrl,
54+
HttpStatusCode.Unauthorized);
55+
}
56+
57+
[Fact]
58+
public async Task Request_InvalidSignature_NotAuthorized()
59+
{
60+
var relativeUrl = "api/test" + await this.urlGenerator.GenerateSasTokenQueryStringAsync(
61+
"/api/test",
62+
DateTime.UtcNow,
63+
DateTime.UtcNow.AddMinutes(5),
64+
QueryParameterHandlingType.DenyAdditionalQueryParameters,
65+
CancellationToken.None);
66+
67+
var queryDict = QueryHelpers.ParseQuery(new Uri("https://" + relativeUrl).Query);
68+
queryDict["sig"] = queryDict["sig"] + "invalid";
69+
70+
relativeUrl = QueryHelpers.AddQueryString("api/test", queryDict);
71+
72+
await this.TestRequestAsync(
73+
relativeUrl,
74+
HttpStatusCode.Unauthorized);
75+
}
76+
77+
[Fact]
78+
public async Task Request_AdditionalParameterInSignature_NotProvidedInUrl_NotAuthorized()
79+
{
80+
var relativeUrl = "api/test" +
81+
await this.urlGenerator.GenerateSasTokenQueryStringAsync(
82+
"/api/test",
83+
new Dictionary<string, StringValues> { ["parameter"] = new StringValues("1") },
84+
DateTime.UtcNow,
85+
DateTime.UtcNow.AddMinutes(5),
86+
QueryParameterHandlingType.DenyAdditionalQueryParameters,
87+
appendQueryParameters: false,
88+
CancellationToken.None);
89+
90+
await this.TestRequestAsync(
91+
relativeUrl,
92+
HttpStatusCode.Unauthorized);
93+
}
94+
95+
[Fact]
96+
public async Task Request_AdditionalParameterInSignature_AutomaticallyAppended_Authorized()
97+
{
98+
var relativeUrl = "api/test" +
99+
await this.urlGenerator.GenerateSasTokenQueryStringAsync(
100+
"/api/test",
101+
new Dictionary<string, StringValues> { ["parameter"] = new StringValues("1") },
102+
DateTime.UtcNow,
103+
DateTime.UtcNow.AddMinutes(5),
104+
QueryParameterHandlingType.DenyAdditionalQueryParameters,
105+
appendQueryParameters: true,
106+
CancellationToken.None);
107+
108+
await this.TestRequestAsync(
109+
relativeUrl,
110+
HttpStatusCode.OK);
111+
}
112+
113+
[Theory]
114+
[InlineData("1", "1", HttpStatusCode.OK)]
115+
[InlineData("1", "2", HttpStatusCode.Unauthorized)]
116+
[InlineData("2", "1", HttpStatusCode.Unauthorized)]
117+
public async Task Request_AdditionalParameterInUrl_ProvidedInSignature(string parameterValueInSignature, string parameterValueInUrl, HttpStatusCode expectedStatusCode)
118+
{
119+
var relativeUrl = "api/test" +
120+
await this.urlGenerator.GenerateSasTokenQueryStringAsync(
121+
"/api/test",
122+
new Dictionary<string, StringValues> { ["parameter"] = new StringValues(parameterValueInSignature) },
123+
DateTime.UtcNow,
124+
DateTime.UtcNow.AddMinutes(5),
125+
QueryParameterHandlingType.DenyAdditionalQueryParameters,
126+
appendQueryParameters: false,
127+
CancellationToken.None) +
128+
$"&parameter={parameterValueInUrl}";
129+
130+
await this.TestRequestAsync(
131+
relativeUrl,
132+
expectedStatusCode);
133+
}
134+
135+
[Theory]
136+
[InlineData(QueryParameterHandlingType.DenyAdditionalQueryParameters, HttpStatusCode.Unauthorized)]
137+
[InlineData(QueryParameterHandlingType.AllowAdditionalQueryParameters, HttpStatusCode.NoContent)]
138+
public async Task Request_AdditionalParameterInUrl_NotProvidedInSignature(QueryParameterHandlingType queryParameterHandlingType, HttpStatusCode expectedStatusCode)
139+
{
140+
var relativeUrl = "api/test" +
141+
await this.urlGenerator.GenerateSasTokenQueryStringAsync(
142+
"/api/test",
143+
new Dictionary<string, StringValues> { ["parameter1"] = new StringValues("1") },
144+
DateTime.UtcNow,
145+
DateTime.UtcNow.AddMinutes(5),
146+
queryParameterHandlingType,
147+
appendQueryParameters: false,
148+
CancellationToken.None) +
149+
"&parameter1=1&parameter2=2";
150+
151+
await this.TestRequestAsync(
152+
relativeUrl,
153+
expectedStatusCode);
154+
}
155+
156+
[Theory]
157+
[InlineData("name", "value", "value")]
158+
[InlineData(null, null, "")]
159+
public async Task Request_NameIdentifierOption(
160+
string? nameIdentifierQueryParameterKey, string? nameIdentifierQueryParameterValue, string expectedNameClaimValue)
161+
{
162+
using var webAppFactory = new TestWebApplicationFactory(new MemoryKeyProvider("key"), nameIdentifierQueryParameterKey);
163+
var urlGenerator = webAppFactory.Services.GetRequiredService<SasTokenUrlGenerator>();
164+
using var httpClient = webAppFactory.CreateDefaultClient();
165+
166+
var queryParameters = new Dictionary<string, StringValues>();
167+
if (nameIdentifierQueryParameterKey != null)
168+
{
169+
queryParameters.Add(nameIdentifierQueryParameterKey, nameIdentifierQueryParameterValue);
170+
}
171+
172+
var relativeUrl = "api/test/name" +
173+
await urlGenerator.GenerateSasTokenQueryStringAsync(
174+
"/api/test/name",
175+
queryParameters,
176+
DateTime.UtcNow,
177+
DateTime.UtcNow.AddMinutes(5));
178+
179+
var response = await TestRequestAsync(
180+
httpClient,
181+
relativeUrl,
182+
expectedNameClaimValue != string.Empty ? HttpStatusCode.OK : HttpStatusCode.NoContent);
183+
184+
var body = await response.Content.ReadAsStringAsync();
185+
Assert.Equal(expectedNameClaimValue, body);
186+
}
187+
188+
private Task<HttpResponseMessage> TestRequestAsync(
189+
string relativeUrl,
190+
HttpStatusCode expectedStatusCode) => TestRequestAsync(this.httpClient, relativeUrl, expectedStatusCode);
191+
192+
private static async Task<HttpResponseMessage> TestRequestAsync(
193+
HttpClient httpClient,
194+
string relativeUrl,
195+
HttpStatusCode expectedStatusCode)
196+
{
197+
var response = await httpClient.GetAsync(relativeUrl);
198+
Assert.Equal(expectedStatusCode, response.StatusCode);
199+
200+
return response;
201+
}
202+
}
203+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace softaware.Authentication.SasToken.AspNetCore.Test
5+
{
6+
[Route("api/[controller]")]
7+
[Authorize]
8+
public class TestController : Controller
9+
{
10+
public ActionResult Index([FromQuery] string? parameter)
11+
{
12+
return this.Ok(parameter);
13+
}
14+
15+
[Route("Name")]
16+
public ActionResult GetName()
17+
{
18+
return this.Ok(this.User.Identity?.Name);
19+
}
20+
}
21+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace softaware.Authentication.SasToken.AspNetCore.Test
8+
{
9+
public class TestStartup(IWebHostEnvironment env)
10+
{
11+
/// <summary>
12+
/// This method gets called by the runtime. Use this method to add services to the container.
13+
/// </summary>
14+
public void ConfigureServices(IServiceCollection services)
15+
{
16+
// Add framework services.
17+
services.AddMvc().AddApplicationPart(typeof(TestController).Assembly);
18+
}
19+
20+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime)
21+
{
22+
app.UseRouting();
23+
24+
app.UseAuthentication();
25+
app.UseAuthorization();
26+
27+
app.UseEndpoints(options =>
28+
{
29+
options
30+
.MapControllers();
31+
});
32+
}
33+
}
34+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.AspNetCore.Mvc.Testing;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using softaware.Authentication.SasToken.KeyProvider;
5+
6+
namespace softaware.Authentication.SasToken.AspNetCore.Test
7+
{
8+
public class TestWebApplicationFactory(IKeyProvider keyProvider, string? nameIdentifierQueryParameter)
9+
: WebApplicationFactory<TestStartup>
10+
{
11+
private readonly IKeyProvider keyProvider = keyProvider;
12+
private readonly string? nameIdentifierQueryParameter = nameIdentifierQueryParameter;
13+
14+
protected override IWebHostBuilder CreateWebHostBuilder()
15+
{
16+
return new WebHostBuilder().UseStartup<TestStartup>();
17+
}
18+
19+
protected override void ConfigureWebHost(IWebHostBuilder builder)
20+
{
21+
builder.ConfigureServices(services =>
22+
{
23+
services.AddTransient(_ => this.keyProvider);
24+
25+
services.AddAuthentication(o =>
26+
{
27+
o.DefaultScheme = SasTokenAuthenticationDefaults.AuthenticationScheme;
28+
})
29+
.AddSasTokenAuthentication(o =>
30+
{
31+
o.NameIdentifierQueryParameter = nameIdentifierQueryParameter;
32+
});
33+
});
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)