diff --git a/src/Robware.Api.Auth/ApiKey/ApiKeyAuthenticationHandler.cs b/src/Robware.Api.Auth/ApiKey/ApiKeyAuthenticationHandler.cs new file mode 100644 index 0000000..a0cb84f --- /dev/null +++ b/src/Robware.Api.Auth/ApiKey/ApiKeyAuthenticationHandler.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Robware.Auth.API; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Robware.Api.Auth.ApiKey { + public class ApiKeyAuthenticationHandler : AuthenticationHandler { + private const string ApiKeyHeaderName = "X-Api-Key"; + private readonly IApiKeyValidator _apiKeyValidator; + private readonly IApiKeys _apiKeys; + + public ApiKeyAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + IApiKeyValidator apiKeyValidator, + IApiKeys apiKeys) : base(options, logger, encoder, clock) { + _apiKeyValidator = apiKeyValidator; + _apiKeys = apiKeys; + } + + protected override async Task HandleAuthenticateAsync() { + if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues)) { + return AuthenticateResult.NoResult(); + } + + var providedApiKey = apiKeyHeaderValues.FirstOrDefault(); + + if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(providedApiKey)) { + return AuthenticateResult.NoResult(); + } + + if (await _apiKeyValidator.Validate(providedApiKey)) { + var key = await _apiKeys.Get(providedApiKey); + var claims = new List + { + new Claim(ClaimTypes.Name, key.Name) + }; + + var identity = new ClaimsIdentity(claims, Options.AuthenticationType); + var principal = new ClaimsPrincipal(new[] { identity }); + var ticket = new AuthenticationTicket(principal, Options.Scheme); + + return AuthenticateResult.Success(ticket); + } + + return AuthenticateResult.Fail("Invalid API Key provided."); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { + Response.StatusCode = 401; + } + + protected override async Task HandleForbiddenAsync(AuthenticationProperties properties) { + Response.StatusCode = 403; + } + } + +} \ No newline at end of file diff --git a/src/Robware.Api.Auth/ApiKey/ApiKeyAuthenticationOptions.cs b/src/Robware.Api.Auth/ApiKey/ApiKeyAuthenticationOptions.cs new file mode 100644 index 0000000..931827c --- /dev/null +++ b/src/Robware.Api.Auth/ApiKey/ApiKeyAuthenticationOptions.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Robware.Api.Auth.ApiKey { + public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions { + public const string DefaultScheme = "API Key"; + public string Scheme => DefaultScheme; + public string AuthenticationType = DefaultScheme; + } +} \ No newline at end of file diff --git a/src/Robware.Api.Auth/ApiKey/AuthenticationBuilderExtensions.cs b/src/Robware.Api.Auth/ApiKey/AuthenticationBuilderExtensions.cs new file mode 100644 index 0000000..15bd21e --- /dev/null +++ b/src/Robware.Api.Auth/ApiKey/AuthenticationBuilderExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authentication; +using System; + +namespace Robware.Api.Auth.ApiKey { + public static class AuthenticationBuilderExtensions { + public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action options) { + return authenticationBuilder.AddScheme(ApiKeyAuthenticationOptions.DefaultScheme, options); + } + } +} \ No newline at end of file diff --git a/src/Robware.Api.Auth/Startup.cs b/src/Robware.Api.Auth/Startup.cs index d39f667..4c28976 100644 --- a/src/Robware.Api.Auth/Startup.cs +++ b/src/Robware.Api.Auth/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MongoDB.Driver; +using Robware.Api.Auth.ApiKey; using Robware.Auth.API; using Robware.Auth.Users; using Robware.Data; @@ -26,6 +27,12 @@ namespace Robware.Api.Auth { .AddSingleton() .AddSingleton() .AddSingleton(); + + services.AddAuthentication(options => { + options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme; + options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme; + }) + .AddApiKeySupport(options=>{}); } private IMongoDatabase SetupMongoDatabase() { @@ -43,6 +50,8 @@ namespace Robware.Api.Auth { app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints => { diff --git a/src/Robware.Data/ApiKeyRepository.cs b/src/Robware.Data/ApiKeyRepository.cs index de618b4..8174ecd 100644 --- a/src/Robware.Data/ApiKeyRepository.cs +++ b/src/Robware.Data/ApiKeyRepository.cs @@ -1,10 +1,19 @@ using Robware.Auth.API; +using System; using System.Threading.Tasks; namespace Robware.Data { public class ApiKeyRepository : IApiKeys { - public Task Get(string key) { - throw new System.NotImplementedException(); + public async Task Get(string key) { + if (key=="denied") + throw new ApiKeyNotFoundException(key); + + return new ApiKey { + Name = "Hardcoded key", + Key = key, + Enabled = true, + IssueTimestamp = DateTime.Now + }; } } } \ No newline at end of file