Add API key authorisation.

This commit is contained in:
Robert Marshall 2021-05-08 15:15:02 +01:00
parent f8e8774d8c
commit c041c42dcf
8 changed files with 124 additions and 5 deletions

View file

@ -18,10 +18,16 @@ steps:
environment:
ConnectionString:
from_secret: ConnectionString
AuthEndpoint:
from_secret: AuthEndpoint
AuthApiKey:
from_secret: AuthApiKey
commands:
- chmod +x ./build.sh
- ./build.sh
- sed -i "s/<DatabaseConnectionString>/$ConnectionString/g" output/appsettings.json
- sed -i "s/<AuthEndpoint>/$AuthEndpoint/g" output/appsettings.json
- sed -i "s/<AuthApiKey>/$AuthApiKey/g" output/appsettings.json
- cp api.blog.service output/
- cp -r ./output/* /output
- name: restart service

View file

@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Robware.Api.Blog.Authentication {
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions> {
private const string ApiKeyHeaderName = "X-Api-Key";
private readonly ApiKeyValidator _apiKeyValidator;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
ApiKeyValidator apiKeyValidator)
: base(options, logger, encoder, clock) {
_apiKeyValidator = apiKeyValidator;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues)) {
return AuthenticateResult.NoResult();
}
var apiKey = apiKeyHeaderValues.FirstOrDefault();
if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(apiKey)) {
return AuthenticateResult.NoResult();
}
if (await _apiKeyValidator.Validate(apiKey)) {
var claims = new List<Claim>
{
new Claim(ApiKeyHeaderName, apiKey)
};
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;
}
}
}

View file

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authentication;
namespace Robware.Api.Blog.Authentication {
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions {
public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
}
}

View file

@ -0,0 +1,18 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace Robware.Api.Blog.Authentication
{
public class ApiKeyValidator {
private readonly HttpClient _httpClient;
public ApiKeyValidator(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task<bool> Validate(string apiKey) {
var response = await _httpClient.GetAsync("api/validate?key=" + apiKey);
return response.IsSuccessStatusCode;
}
}
}

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authentication;
using System;
namespace Robware.Api.Blog.Authentication {
public static class AuthenticationBuilderExtensions {
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options) {
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
}
}
}

View file

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>

View file

@ -4,8 +4,10 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MongoDB.Driver;
using Robware.Api.Blog.Authentication;
using Robware.Blog;
using Robware.Data;
using System;
namespace Robware.Api.Blog {
public class Startup {
@ -22,6 +24,17 @@ namespace Robware.Api.Blog {
services
.AddSingleton<IMongoDatabase>(SetupMongoDatabase())
.AddSingleton<IBlogRepository, BlogRepository>();
services.AddHttpClient<ApiKeyValidator>(client => {
client.BaseAddress = new Uri(Configuration["authEndpoint"]);
client.DefaultRequestHeaders.Add("x-api-key", new[] {Configuration["authApiKey"]});
});
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddApiKeySupport(options => { });
}
private IMongoDatabase SetupMongoDatabase() {
@ -39,10 +52,12 @@ namespace Robware.Api.Blog {
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapControllers().RequireAuthorization();
});
}
}

View file

@ -16,5 +16,7 @@
},
"ConnectionStrings": {
"database": "<DatabaseConnectionString>"
}
},
"authEndpoint": "<AuthEndpoint>",
"authApiKey": "<AuthApiKey>"
}