Skip to content

Commit

Permalink
Merge pull request #40 from mihirdilip/8.0.0
Browse files Browse the repository at this point in the history
8.0.0
  • Loading branch information
mihirdilip authored Nov 22, 2023
2 parents 0e7f017 + e6fa978 commit 8e812e8
Show file tree
Hide file tree
Showing 45 changed files with 858 additions and 190 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Mihir Dilip
Copyright (c) 2023 Mihir Dilip

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Easy to use and very light weight Microsoft style API Key Authentication Impleme

## .NET (Core) Frameworks Supported
.NET Framework 4.6.1 and/or NetStandard 2.0 onwards
Multi targeted: net7.0; net6.0; net5.0; netcoreapp3.1; netcoreapp3.0; netstandard2.0; net461
Multi targeted: net8.0; net7.0; net6.0; net5.0; netcoreapp3.1; netcoreapp3.0; netstandard2.0; net461

<br/>

Expand Down Expand Up @@ -380,6 +380,7 @@ public void ConfigureServices(IServiceCollection services)
## Release Notes
| Version | &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Notes |
|---------|-------|
|8.0.0 | <ul><li>net8.0 support added</li><li>Sample project for net8.0 added</li><li>ApiKeySamplesClient.http file added for testing sample projects</li><li>Readme updated</li></ul> |
|7.0.0 | <ul><li>net7.0 support added</li><li>Information log on handler is changed to Debug log when API Key is not found on the request</li><li>Added package validations</li><li>Sample project for net7.0 added</li><li>Readme updated</li><li>Readme added to package</li></ul> |
|6.0.1 | <ul><li>net6.0 support added</li><li>Information log on handler is changed to Debug log when IgnoreAuthenticationIfAllowAnonymous is enabled</li><li>Sample project added</li><li>Readme updated</li><li>Copyright year updated on License</li></ul> |
|5.1.0 | <ul><li>WWW-Authenticate challenge header now returns SchemeName as scheme part instead of ApiKeyOptions.KeyName</li><li>WWW-Authenticate challenge header now has 2 new parameters 'in' and 'key_name' in value part</li><li>ForLegacyUseKeyNameAsSchemeNameOnWWWAuthenticateHeader added to the ApiKeyOptions</li><li>In Authorization Header now able to use either SchemeName or ApiKeyOptions.KeyName when matching AuthorizationHeader Scheme</li><li>Visibility of all the handlers changed to public [#21](https://github.com/mihirdilip/aspnetcore-authentication-apikey/issues/21)</li><li>Tests added</li><li>Readme updated</li><li>Copyright year updated on License</li></ul> |
Expand Down
27 changes: 27 additions & 0 deletions samples/ApiKeySamplesClient.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@HostAddress = https://localhost:44304
@ApiKey = Key1

# Get Values No Auth
GET {{HostAddress}}/api/values
Accept: application/json

###

# Get Values
GET {{HostAddress}}/api/values
Accept: application/json
X-API-Key: {{ApiKey}}

###

# Claims
GET {{HostAddress}}/api/values/claims
Accept: application/json
X-API-Key: {{ApiKey}}

###

# Forbid
GET {{HostAddress}}/api/values/forbid
Accept: application/json
X-API-Key: {{ApiKey}}
15 changes: 15 additions & 0 deletions samples/SampleWebApi.Shared/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "<Pending>", Scope = "member", Target = "~F:SampleWebApi.Repositories.InMemoryApiKeyRepository._cache")]
[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'", Justification = "<Pending>", Scope = "member", Target = "~F:SampleWebApi.Repositories.InMemoryApiKeyRepository._cache")]
[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "<Pending>", Scope = "member", Target = "~M:SampleWebApi.Models.ApiKey.#ctor(System.String,System.String,System.Collections.Generic.List{System.Security.Claims.Claim})")]
[assembly: SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "<Pending>", Scope = "member", Target = "~M:SampleWebApi.Models.ApiKey.#ctor(System.String,System.String,System.Collections.Generic.List{System.Security.Claims.Claim})")]
[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "<Pending>", Scope = "member", Target = "~M:SampleWebApi.Services.ApiKeyProvider.#ctor(Microsoft.Extensions.Logging.ILogger{AspNetCore.Authentication.ApiKey.IApiKeyProvider},SampleWebApi.Repositories.IApiKeyRepository)")]

[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "<Pending>", Scope = "member", Target = "~M:SampleWebApi.Services.ApiKeyProvider.ProvideAsync(System.String)~System.Threading.Tasks.Task{AspNetCore.Authentication.ApiKey.IApiKey}")]

8 changes: 5 additions & 3 deletions samples/SampleWebApi.Shared/Models/ApiKey.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using AspNetCore.Authentication.ApiKey;
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
using AspNetCore.Authentication.ApiKey;
using System.Collections.Generic;
using System.Security.Claims;

namespace SampleWebApi.Models
{
class ApiKey : IApiKey
{
public ApiKey(string key, string owner, List<Claim> claims = null)
{
public ApiKey(string key, string owner, List<Claim> claims = null)
{
Key = key;
OwnerName = owner;
Claims = claims ?? new List<Claim>();
Expand All @@ -18,3 +19,4 @@ public ApiKey(string key, string owner, List<Claim> claims = null)
public IReadOnlyCollection<Claim> Claims { get; }
}
}
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ namespace SampleWebApi.Repositories
/// </summary>
public interface IApiKeyRepository
{
Task<IApiKey> GetApiKeyAsync(string key);
Task<IApiKey> GetApiKeyAsync(string key);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AspNetCore.Authentication.ApiKey;
#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type.
using AspNetCore.Authentication.ApiKey;
using SampleWebApi.Models;
using System;
using System.Collections.Generic;
Expand All @@ -7,21 +8,22 @@

namespace SampleWebApi.Repositories
{
/// <summary>
/// NOTE: DO NOT USE THIS IMPLEMENTATION. THIS IS FOR DEMO PURPOSE ONLY
/// </summary>
public class InMemoryApiKeyRepository : IApiKeyRepository
/// <summary>
/// NOTE: DO NOT USE THIS IMPLEMENTATION. THIS IS FOR DEMO PURPOSE ONLY
/// </summary>
public class InMemoryApiKeyRepository : IApiKeyRepository
{
private List<IApiKey> _cache = new List<IApiKey>
{
private readonly List<IApiKey> _cache = new List<IApiKey>()
{
new ApiKey("Key1", "Admin"),
new ApiKey("Key2", "User"),
};

public Task<IApiKey> GetApiKeyAsync(string key)
{
var apiKey = _cache.FirstOrDefault(k => k.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
return Task.FromResult(apiKey);
}
}
return Task.FromResult(apiKey);
}
}
}
#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type.
1 change: 1 addition & 0 deletions samples/SampleWebApi.Shared/SampleWebApi.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Import_RootNamespace>SampleWebApi.Shared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)GlobalSuppressions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\ApiKey.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\ApiKeyProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Repositories\InMemoryApiKeyRepository.cs" />
Expand Down
2 changes: 2 additions & 0 deletions samples/SampleWebApi_2_0/SampleWebApi_2_0.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions samples/SampleWebApi_2_2/SampleWebApi_2_2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions samples/SampleWebApi_3_1/SampleWebApi_3_1.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
</PropertyGroup>

<Import Project="..\SampleWebApi.Shared\SampleWebApi.Shared.projitems" Label="Shared" />
Expand Down
1 change: 1 addition & 0 deletions samples/SampleWebApi_5_0/SampleWebApi_5_0.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
</PropertyGroup>

<Import Project="..\SampleWebApi.Shared\SampleWebApi.Shared.projitems" Label="Shared" />
Expand Down
36 changes: 36 additions & 0 deletions samples/SampleWebApi_8_0/Controllers/ValuesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text;

namespace SampleWebApi_8_0.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[AllowAnonymous]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}

[HttpGet("claims")]
public ActionResult<string> Claims()
{
var sb = new StringBuilder();
foreach (var claim in User.Claims)
{
sb.AppendLine($"{claim.Type}: {claim.Value}");
}
return sb.ToString();
}

[HttpGet("forbid")]
public new IActionResult Forbid()
{
return base.Forbid();
}
}
}
162 changes: 162 additions & 0 deletions samples/SampleWebApi_8_0/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using AspNetCore.Authentication.ApiKey;
using Microsoft.AspNetCore.Authorization;
using SampleWebApi.Repositories;
using SampleWebApi.Services;

var builder = WebApplication.CreateBuilder(args);

// Add User repository to the dependency container.
builder.Services.AddTransient<IApiKeyRepository, InMemoryApiKeyRepository>();

// Add the ApiKey scheme authentication here..
// It requires Realm to be set in the options if SuppressWWWAuthenticateHeader is not set.
// If an implementation of IApiKeyProvider interface is registered in the dependency register as well as OnValidateKey delegete on options.Events is also set then this delegate will be used instead of an implementation of IApiKeyProvider.
builder.Services.AddAuthentication(ApiKeyDefaults.AuthenticationScheme)

// The below AddApiKeyInHeaderOrQueryParams without type parameter will require OnValidateKey delegete on options.Events to be set unless an implementation of IApiKeyProvider interface is registered in the dependency register.
// Please note if OnValidateKey delegete on options.Events is also set then this delegate will be used instead of ApiKeyProvider.*
//.AddApiKeyInHeaderOrQueryParams(options =>

// The below AddApiKeyInHeaderOrQueryParams with type parameter will add the ApiKeyProvider to the dependency register.
// Please note if OnValidateKey delegete on options.Events is also set then this delegate will be used instead of ApiKeyProvider.
.AddApiKeyInHeaderOrQueryParams<ApiKeyProvider>(options =>
{
options.Realm = "Sample Web API";
options.KeyName = "X-API-KEY";
//// Optional option to suppress the browser login dialog for ajax calls.
//options.SuppressWWWAuthenticateHeader = true;
//// Optional option to ignore extra check of ApiKey string after it is validated.
//options.ForLegacyIgnoreExtraValidatedApiKeyCheck = true;
//// Optional option to ignore authentication if AllowAnonumous metadata/filter attribute is added to an endpoint.
//options.IgnoreAuthenticationIfAllowAnonymous = true;
//// Optional events to override the ApiKey original logic with custom logic.
//// Only use this if you know what you are doing at your own risk. Any of the events can be assigned.
options.Events = new ApiKeyEvents
{
//// A delegate assigned to this property will be invoked just before validating the api key.
//OnValidateKey = async (context) =>
//{
// // custom code to handle the api key, create principal and call Success method on context.
// var apiKeyRepository = context.HttpContext.RequestServices.GetRequiredService<IApiKeyRepository>();
// var apiKey = await apiKeyRepository.GetApiKeyAsync(context.ApiKey);
// var isValid = apiKey != null && apiKey.Key.Equals(context.ApiKey, StringComparison.OrdinalIgnoreCase);
// if (isValid)
// {
// context.Response.Headers.Add("ValidationCustomHeader", "From OnValidateKey");
// var claims = new[]
// {
// new Claim(ClaimTypes.NameIdentifier, apiKey.OwnerName, ClaimValueTypes.String, context.Options.ClaimsIssuer),
// new Claim(ClaimTypes.Name, apiKey.OwnerName, ClaimValueTypes.String, context.Options.ClaimsIssuer),
// new Claim("CustomClaimType", "Custom Claim Value - from OnValidateKey")
// };
// context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
// context.Success();
// }
// else
// {
// context.NoResult();
// }
//},
//// A delegate assigned to this property will be invoked just before validating the api key.
//// NOTE: Same as above delegate but slightly different implementation which will give same result.
//OnValidateKey = async (context) =>
//{
// // custom code to handle the api key, create principal and call Success method on context.
// var apiKeyRepository = context.HttpContext.RequestServices.GetRequiredService<IApiKeyRepository>();
// var apiKey = await apiKeyRepository.GetApiKeyAsync(context.ApiKey);
// var isValid = apiKey != null && apiKey.Key.Equals(context.ApiKey, StringComparison.OrdinalIgnoreCase);
// if (isValid)
// {
// context.Response.Headers.Add("ValidationCustomHeader", "From OnValidateKey");
// var claims = new[]
// {
// new Claim("CustomClaimType", "Custom Claim Value - from OnValidateKey")
// };
// context.ValidationSucceeded(apiKey.OwnerName, claims); // claims are optional
// }
// else
// {
// context.ValidationFailed();
// }
//},
//// A delegate assigned to this property will be invoked before a challenge is sent back to the caller when handling unauthorized response.
//OnHandleChallenge = async (context) =>
//{
// // custom code to handle authentication challenge unauthorized response.
// context.Response.StatusCode = StatusCodes.Status401Unauthorized;
// context.Response.Headers.Add("ChallengeCustomHeader", "From OnHandleChallenge");
// await context.Response.WriteAsync("{\"CustomBody\":\"From OnHandleChallenge\"}");
// context.Handled(); // important! do not forget to call this method at the end.
//},
//// A delegate assigned to this property will be invoked if Authorization fails and results in a Forbidden response.
//OnHandleForbidden = async (context) =>
//{
// // custom code to handle forbidden response.
// context.Response.StatusCode = StatusCodes.Status403Forbidden;
// context.Response.Headers.Add("ForbidCustomHeader", "From OnHandleForbidden");
// await context.Response.WriteAsync("{\"CustomBody\":\"From OnHandleForbidden\"}");
// context.Handled(); // important! do not forget to call this method at the end.
//},
//// A delegate assigned to this property will be invoked when the authentication succeeds. It will not be called if OnValidateKey delegate is assigned.
//// It can be used for adding claims, headers, etc to the response.
//OnAuthenticationSucceeded = (context) =>
//{
// //custom code to add extra bits to the success response.
// context.Response.Headers.Add("SuccessCustomHeader", "From OnAuthenticationSucceeded");
// var customClaims = new List<Claim>
// {
// new Claim("CustomClaimType", "Custom Claim Value - from OnAuthenticationSucceeded")
// };
// context.AddClaims(customClaims);
// //or can add like this - context.Principal.AddIdentity(new ClaimsIdentity(customClaims));
// return Task.CompletedTask;
//},
//// A delegate assigned to this property will be invoked when the authentication fails.
//OnAuthenticationFailed = (context) =>
//{
// // custom code to handle failed authentication.
// context.Fail("Failed to authenticate");
// return Task.CompletedTask;
//}
};
});

builder.Services.AddControllers(options =>
{
// ALWAYS USE HTTPS (SSL) protocol in production when using ApiKey authentication.
//options.Filters.Add<RequireHttpsAttribute>();
}); //.AddXmlSerializerFormatters() // To enable XML along with JSON;

// All the requests will need to be authorized.
// Alternatively, add [Authorize] attribute to Controller or Action Method where necessary.
builder.Services.AddAuthorizationBuilder()
.AddFallbackPolicy(
"FallbackPolicy",
new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()
);

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthentication(); // NOTE: DEFAULT TEMPLATE DOES NOT HAVE THIS, THIS LINE IS REQUIRED AND HAS TO BE ADDED!!!

app.UseAuthorization();

app.MapControllers();

app.Run();
Loading

0 comments on commit 8e812e8

Please sign in to comment.