Skip to content

Commit

Permalink
add api key authentication schame for aspnet
Browse files Browse the repository at this point in the history
  • Loading branch information
jxnkwlp committed Sep 19, 2023
1 parent d3a7697 commit 3bb8a46
Show file tree
Hide file tree
Showing 23 changed files with 827 additions and 13 deletions.
7 changes: 7 additions & 0 deletions Passingwind.CommonLibs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Passingwind.SwaggerExtensio
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Passingwind.AspNetCore.Authentication.Saml2", "src\Passingwind.AspNetCore.Authentication.Saml2\Passingwind.AspNetCore.Authentication.Saml2.csproj", "{4ED083F0-7B73-4380-A4F5-164474FDCF82}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Passingwind.AspNetCore.Authentication.ApiKey", "src\Authentication.ApiKey\source\Passingwind.AspNetCore.Authentication.ApiKey.csproj", "{F7FE0AD3-B69F-4F10-8EA4-E581EBE4A2AC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -33,6 +35,10 @@ Global
{4ED083F0-7B73-4380-A4F5-164474FDCF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4ED083F0-7B73-4380-A4F5-164474FDCF82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4ED083F0-7B73-4380-A4F5-164474FDCF82}.Release|Any CPU.Build.0 = Release|Any CPU
{F7FE0AD3-B69F-4F10-8EA4-E581EBE4A2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7FE0AD3-B69F-4F10-8EA4-E581EBE4A2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7FE0AD3-B69F-4F10-8EA4-E581EBE4A2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7FE0AD3-B69F-4F10-8EA4-E581EBE4A2AC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -41,6 +47,7 @@ Global
{41CC2AD6-8FDC-4F00-8CF1-94C89666137E} = {D7A92342-2C8A-4121-8824-95AEF5856AAF}
{300C8EF1-B040-4F50-BA65-175EE5A82A0F} = {CE8B3FAE-E7B7-4EB6-BEB5-716F2B91A315}
{4ED083F0-7B73-4380-A4F5-164474FDCF82} = {CE8B3FAE-E7B7-4EB6-BEB5-716F2B91A315}
{F7FE0AD3-B69F-4F10-8EA4-E581EBE4A2AC} = {CE8B3FAE-E7B7-4EB6-BEB5-716F2B91A315}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB481C2D-55C1-486C-873D-408B11F77F30}
Expand Down
26 changes: 26 additions & 0 deletions samples/SampleWeb/Controllers/ValuesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace SampleWeb.Controllers;

[Route("api/values")]
public class ValuesController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new[] { "value1", "value2" });
}

[HttpGet("auth")]
[Authorize]
public IActionResult Authorization()
{
return Ok(new
{
User.Identity.Name,
User.Identity.AuthenticationType,
Claims = User.Claims.Select(x => new { x.Type, x.Value })
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace SampleWeb.Data.Migrations;
Expand Down
3 changes: 1 addition & 2 deletions samples/SampleWeb/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SampleWeb.Pages;
public class IndexModel : PageModel
Expand Down
3 changes: 1 addition & 2 deletions samples/SampleWeb/Pages/Privacy.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SampleWeb.Pages;
public class PrivacyModel : PageModel
Expand Down
57 changes: 56 additions & 1 deletion samples/SampleWeb/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Passingwind.AspNetCore.Authentication.ApiKey;
using SampleWeb.Data;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -10,12 +12,37 @@
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
builder.Services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllers();

builder.Services
.AddAuthentication()
.AddApiKey<TestApiKeyProvider>();

builder.Services.ConfigureApplicationCookie(options =>
{
options.ForwardDefaultSelector = (s) =>
{
var authorization = (string?)s.Request.Headers.Authorization;
if (authorization?.StartsWith(ApiKeyDefaults.AuthenticationSchemeName) == true)
return ApiKeyDefaults.AuthenticationScheme;

return IdentityConstants.ApplicationScheme;
};
});

var app = builder.Build();

using var scope = app.Services.CreateScope();

var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
if (await userManager.FindByNameAsync("bob") == null)
{
await userManager.CreateAsync(new IdentityUser("bob") { Email = "[email protected]", EmailConfirmed = true, Id = Guid.NewGuid().ToString(), });
}

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
Expand All @@ -35,6 +62,34 @@

app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();


public class TestApiKeyProvider : IApiKeyProvider
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;

public TestApiKeyProvider(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}

public async Task<ApiKeyValidationResult> ValidateAsync(string apiKey, CancellationToken cancellationToken = default)
{
if (apiKey == "1234567890")
{
var user = await _userManager.FindByNameAsync("bob");

var principal = await _signInManager.ClaimsFactory.CreateAsync(user!);

return ApiKeyValidationResult.Success(new ClaimsIdentity(principal.Identity));
}

return ApiKeyValidationResult.Failed(new Exception("invalid api key"));
}
}
10 changes: 4 additions & 6 deletions samples/SampleWeb/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,16 @@
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5083",
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:6828",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7032;http://localhost:5083",
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:44358;http://localhost:6828",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
4 changes: 4 additions & 0 deletions samples/SampleWeb/SampleWeb.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Authentication.ApiKey\source\Passingwind.AspNetCore.Authentication.ApiKey.csproj" />
</ItemGroup>

</Project>
45 changes: 45 additions & 0 deletions src/Authentication.ApiKey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# AspNetCore.Authentication.ApiKey

ASP.NET Core authentication handler for the ApiKey protocol

## Quickstart

``` cs
builder.Services
.AddAuthentication()
// api ApiKey scheme
.AddApiKey<TestApiKeyProvider>();

// configure this if you default scheme is not 'ApiKey'
// builder.Services.ConfigureApplicationCookie(options =>
// {
// options.ForwardDefaultSelector = (s) =>
// {
// var authorization = (string?)s.Request.Headers.Authorization;
// if (authorization?.StartsWith(ApiKeyDefaults.AuthenticationSchemeName) == true)
// return ApiKeyDefaults.AuthenticationScheme;
//
// // you default scheme
// return IdentityConstants.ApplicationScheme;
// };
// });
```

TestApiKeyProvider.cs

```cs
public class TestApiKeyProvider : IApiKeyProvider
{
public async Task<ApiKeyValidationResult> ValidateAsync(string apiKey, CancellationToken cancellationToken = default)
{
// verification apiKey
...

// if success
return ApiKeyValidationResult.Success(...);

// if fail
return ApiKeyValidationResult.Failed(new Exception("invalid api key"));
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;

namespace Passingwind.AspNetCore.Authentication.ApiKey;

/// <summary>
/// A <see cref="ResultContext{TOptions}"/> when authentication has failed.
/// </summary>
public class ApiKeyAuthenticationFailedContext : ResultContext<ApiKeyOptions>
{
/// <summary>
/// Initializes a new instance of <see cref="ApiKeyAuthenticationFailedContext"/>.
/// </summary>
/// <inheritdoc />
public ApiKeyAuthenticationFailedContext(
HttpContext context,
AuthenticationScheme scheme,
ApiKeyOptions options)
: base(context, scheme, options)
{
}

/// <summary>
/// Gets or sets the exception associated with the authentication failure.
/// </summary>
public Exception Exception { get; set; } = default!;
}
40 changes: 40 additions & 0 deletions src/Authentication.ApiKey/source/ApiKeyChallengeContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;

namespace Passingwind.AspNetCore.Authentication.ApiKey;

/// <summary>
/// A <see cref="PropertiesContext{TOptions}"/> when access to a resource authenticated using ApiKey is challenged.
/// </summary>
public class ApiKeyChallengeContext : PropertiesContext<ApiKeyOptions>
{
/// <summary>
/// Initializes a new instance of <see cref="ApiKeyChallengeContext"/>.
/// </summary>
/// <inheritdoc />
public ApiKeyChallengeContext(HttpContext context, AuthenticationScheme scheme, ApiKeyOptions options, AuthenticationProperties? properties) : base(context, scheme, options, properties)
{
}

/// <summary>
/// Any failures encountered during the authentication process.
/// </summary>
public Exception? AuthenticateFailure { get; set; }

/// <summary>
/// Gets or sets the "error" value returned to the caller as part
/// of the WWW-Authenticate header.
/// </summary>
public string? Error { get; set; }

/// <summary>
/// If true, will skip any default logic for this challenge.
/// </summary>
public bool Handled { get; private set; }

/// <summary>
/// Skips any default logic for this challenge.
/// </summary>
public void HandleResponse() => Handled = true;
}
27 changes: 27 additions & 0 deletions src/Authentication.ApiKey/source/ApiKeyDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Passingwind.AspNetCore.Authentication.ApiKey;

/// <summary>
/// Default value for ApiKey authentication
/// </summary>
public static class ApiKeyDefaults
{
/// <summary>
/// Default value for AuthenticationScheme
/// </summary>
public const string AuthenticationScheme = "ApiKey";

/// <summary>
///
/// </summary>
public const string HeaderName = "X-ApiKey";

/// <summary>
///
/// </summary>
public const string QueryStringName = "x-apikey";

/// <summary>
///
/// </summary>
public const string AuthenticationSchemeName = "ApiKey";
}
53 changes: 53 additions & 0 deletions src/Authentication.ApiKey/source/ApiKeyEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Threading.Tasks;

namespace Passingwind.AspNetCore.Authentication.ApiKey;

/// <summary>
/// Specifies events which the <see cref="ApiKeyHandler"/> invokes to enable developer control over the authentication process.
/// </summary>
public class ApiKeyEvents
{
/// <summary>
///
/// </summary>
public Func<ApiKeyMessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;
/// <summary>
///
/// </summary>
public Func<ApiKeyTokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.CompletedTask;
/// <summary>
///
/// </summary>
public Func<ApiKeyAuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
/// <summary>
///
/// </summary>
public Func<ApiKeyChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask;
/// <summary>
///
/// </summary>
public Func<ApiKeyForbiddenContext, Task> OnForbidden { get; set; } = context => Task.CompletedTask;


/// <summary>
/// Invoked when a protocol message is first received.
/// </summary>
public virtual Task MessageReceivedAsync(ApiKeyMessageReceivedContext context) => OnMessageReceived(context);
/// <summary>
/// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
/// </summary>
public virtual Task TokenValidatedAsync(ApiKeyTokenValidatedContext context) => OnTokenValidated(context);
/// <summary>
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
/// </summary>
public virtual Task AuthenticationFailedAsync(ApiKeyAuthenticationFailedContext context) => OnAuthenticationFailed(context);
/// <summary>
/// Invoked before a challenge is sent back to the caller.
/// </summary>
public virtual Task ChallengeAsync(ApiKeyChallengeContext context) => OnChallenge(context);
/// <summary>
/// Invoked if Authorization fails and results in a Forbidden response
/// </summary>
public virtual Task ForbiddenAsync(ApiKeyForbiddenContext context) => OnForbidden(context);
}
Loading

0 comments on commit 3bb8a46

Please sign in to comment.