Skip to content

Commit

Permalink
Added PasswordCredential And APIkey authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jan 12, 2024
1 parent ddf8f22 commit 03fd6b9
Show file tree
Hide file tree
Showing 344 changed files with 16,232 additions and 646 deletions.
73 changes: 73 additions & 0 deletions docs/decisions/0100-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Authentication

* status: accepted
* date: 2024-01-01
* deciders: jezzsantos

# Context and Problem Statement

AuthN (short for Authentication) is not to be confused with AuthZ (short for Authorization).

AuthN is essentially the process of identifying an end-user given some kind of proof that they provide. Common forms of proof include: usernames+passwords, pin numbers, locations, keys etc). Once that proof is verified, the end-user can correctly and uniquely be identified, and information about the end-user can be used downstream to "Authorize" the user to access parts of the system, and to apply rules to their use of the software.

> Part of the reason that many developers confuse and conflate AuthN and AuthZ is to do with how they've experienced them in the various frameworks they've used in the past that have not been so explicit in the distinction. Another part of the reason is that some "implementations" of these mechanisms combine them as well.
>
> For example, in '[Basic Authentication](https://datatracker.ietf.org/doc/html/rfc7617)', we send a user's authentication credentials (username + password) along with the same request that we want to be performed. The credentials are first authenticated to identify the user, and then the request is authorized, all in the same interaction.
>
> It gets even more conflated for things like '[HMAC authentication](https://datatracker.ietf.org/doc/html/rfc2104)' where typically the user's identity is pre-assumed to be known ahead of time, so when the request arrives, the identity of the user is already known (you could say, implied by the HMAC signing key, or included in other data the request). There may be no explicit authentication step at all.
>
> However, in most systems (post 2012) that adopted the '[OAuth2 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749)' and then later the ''[OpenID Connect Authentication Framework](https://openid.net/specs/openid-connect-core-1_0.html)' the AuthZ and AuthN steps are explicitly separated into different processes using tokens and claims.
Today there are many options for integrating AuthN into a SaaS product, it is not a straightforward choice to make, and and each and everyone of those options represents a set of trade-offs in many things, including:

* Cost
* Maintainability
* Complexity
* Flexibility
* Capability and Familiarity,
* Vendor Lock-In,
* Security, etc

Given this is a SaaS context, for small teams, and given we are building a backend API plus a separate front end web app, we need to aim to:

* Choose a decent (well-known) integration that can added plugged in (at low cost), and easily replaced with another plugin later without requiring re-engineering the architecture.
* Offers reasonable flexibility (to expand to changing needs of a particular business).
* It must be secure and multi-client friendly (e.g., web app and mobile app and machine-to-machine) when applied to a backend API (i.e. not bound to cookies and sessions of a front end web server).
* We prefer to utilize refreshable, transparent, and signed JWT tokens (with the option of encrypting them as opaque if necessary later).
* We will need it to be extensible to accommodate Single-Sign-On (SSO) scenarios.
* It needs to be very secure, and we need to ship a basic "user credentials" solution out of the box to get started with.

Lastly, we need to introduce some minimal abstractions to make that integration easier to understand and to rip out and change later (e.g., ports and adapters).

## Considered Options

The options are:

1. Custom Implementation
2. ASP.NET Core Identity
3. Auth0
4. Duende IdentityServer (https://duendesoftware.com/products/identityserver)
5. OpenIddict (https://documentation.openiddict.com/)
6. etc

## Decision Outcome

`Custom Implementation`

- Can support many authorization protocols, like: HMAC, Basic, APIKey, Claims, Cookies, etc. as prescribed by 3rd party integrations and web hooks.
- Can support many authorization assertions, like: Roles (RBAC), FeatureLevel access, etc.
- Can support SSO authentication from 3rd parties, like: Microsoft, Google, Facebook etc.
- Would not be OIDC authentication compliant at first, but could be made to be OIDC compliant later, by either integrating with an external provider or implementing the endpoints and flows.
- No additional operational costs, (unlike IdentityServer, Auth0 require etc)
- Can be ripped out and replaced out for an implementation of IdentityServer, Auth0, Okta, or other solution later.
- Has decent support for most of the most common capabilities for an early stage SaaS business e.g. transparent JWT tokens, custom claims, Single Sign On integrations, MFA, Authenticator apps, password management, etc)
- Is a superior option to `ASP.NET Core Identity` since we would not be limited, as ANCI is, to opaque non-JWT tokens, and we can control the behaviour of each of the APIs, which cannot be done in ANCI either.

Downsides:

* Increased risk of creating security vulnerabilities by developers (making mistakes)
* Increased risk of storing user password data in this system

## More Information

See Andrew Locks discussion on using the [ASP.NET Core Identity APIs in .NET 8.0](https://andrewlock.net/should-you-use-the-dotnet-8-identity-api-endpoints/#what-are-the-new-identity-api-endpoints-)
38 changes: 19 additions & 19 deletions docs/design-principles/0020-api-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,26 @@ This is an example of the declarative way we prefer to define our endpoints, in
public sealed class CarsApi : IWebApiService
{
private readonly ICarsApplication _carsApplication;
private readonly ICallerContext _context;
private readonly ICallerContextFactory _contextFactory;

public CarsApi(ICallerContext context, ICarsApplication carsApplication)
public CarsApi(ICallerContextFactory contextFactory, ICarsApplication carsApplication)
{
_context = context;
_contextFactory = contextFactory;
_carsApplication = carsApplication;
}

[AuthorizeForAnyRole(OrganizationRoles.Manager)]
public async Task<ApiDeleteResult> Delete(DeleteCarRequest request,
CancellationToken cancellationToken)
{
var car = await _carsApplication.DeleteCarAsync(_context, request.Id, cancellationToken);
var car = await _carsApplication.DeleteCarAsync(_contextFactory.Create(), request.Id, cancellationToken);
return () => car.HandleApplicationResult();
}

[AuthorizeForAnyRole(OrganizationRoles.Reserver, OrganizationRoles.Manager)]
public async Task<ApiGetResult<Car, GetCarResponse>> Get(GetCarRequest request, CancellationToken cancellationToken)
{
var car = await _carsApplication.GetCarAsync(_context, request.Id, cancellationToken);
var car = await _carsApplication.GetCarAsync(_contextFactory.Create(), request.Id, cancellationToken);

return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c });
}
Expand All @@ -57,7 +57,7 @@ public sealed class CarsApi : IWebApiService
public async Task<ApiPostResult<Car, GetCarResponse>> Register(RegisterCarRequest request,
CancellationToken cancellationToken)
{
var car = await _carsApplication.RegisterCarAsync(_context, request.Make, request.Model, request.Year,
var car = await _carsApplication.RegisterCarAsync(_contextFactory.Create(), request.Make, request.Model, request.Year,
cancellationToken);

return () => car.HandleApplicationResult<GetCarResponse, Car>(c =>
Expand All @@ -68,7 +68,7 @@ public sealed class CarsApi : IWebApiService
public async Task<ApiSearchResult<Car, SearchAllCarsResponse>> SearchAll(SearchAllCarsRequest request,
CancellationToken cancellationToken)
{
var cars = await _carsApplication.SearchAllCarsAsync(_context, request.ToSearchOptions(),
var cars = await _carsApplication.SearchAllCarsAsync(_contextFactory.Create(), request.ToSearchOptions(),
request.ToGetOptions(), cancellationToken);

return () =>
Expand All @@ -79,7 +79,7 @@ public sealed class CarsApi : IWebApiService
public async Task<ApiPutPatchResult<Car, GetCarResponse>> TakeOffline(TakeOfflineCarRequest request,
CancellationToken cancellationToken)
{
var car = await _carsApplication.TakeOfflineCarAsync(_context, request.Id!, request.Reason, request.StartAtUtc,
var car = await _carsApplication.TakeOfflineCarAsync(_contextFactory.Create(), request.Id!, request.Reason, request.StartAtUtc,
request.EndAtUtc, cancellationToken);
return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c });
}
Expand All @@ -101,12 +101,12 @@ AND, we prefer NOT to have to create MediatR class like this, for every single o
```c#
public class GetCarRequestHandler : IRequestHandler<GetCarRequest, IResult>
{
private readonly ICallerContext _context;
private readonly ICallerContextFactory _contextFactory;
private readonly ICarsApplication _carsApplication;

public GetCarRequestHandler(ICallerContext context, ICarsApplication carsApplication)
public GetCarRequestHandler(ICallerContextFactory contextFactory, ICarsApplication carsApplication)
{
this._context = context;
this._contextFactory = contextFactory;
this._carsApplication = carsApplication;
}

Expand Down Expand Up @@ -236,18 +236,18 @@ Then we use Roslyn analyzers (and other tooling) to guide the author in creating
public sealed class CarsApi : IWebApiService
{
private readonly ICarsApplication _carsApplication;
private readonly ICallerContext _context;
private readonly ICallerContextFactory _contextFactory;

public CarsApi(ICallerContext context, ICarsApplication carsApplication)
public CarsApi(ICallerContextFactory contextFactory, ICarsApplication carsApplication)
{
_context = context;
_contextFactory = contextFactory;
_carsApplication = carsApplication;
}

[AuthorizeForAnyRole(OrganizationRoles.Reserver, OrganizationRoles.Manager)]
public async Task<ApiGetResult<Car, GetCarResponse>> Get(GetCarRequest request, CancellationToken cancellationToken)
{
var car = await _carsApplication.GetCarAsync(_context, request.Id, cancellationToken);
var car = await _carsApplication.GetCarAsync(_contextFactory.Create(), request.Id, cancellationToken);

return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c });
}
Expand Down Expand Up @@ -306,9 +306,9 @@ Then we use Roslyn analyzers (and other tooling) to guide the author in creating

1. For example:
```c#
public CarsApi(ICallerContext context, ICarsApplication carsApplication)
public CarsApi(ICallerContextFactory contextFactory, ICarsApplication carsApplication)
{
_context = context;
_contextFactory = contextFactory;
_carsApplication = carsApplication;
}
```
Expand All @@ -321,7 +321,7 @@ Then we use Roslyn analyzers (and other tooling) to guide the author in creating
- For example, in the project and folder: `CarsApi.IntegrationTests/CarsApiSpec.cs`

```c#
[Trait("Category", "Integration.Web")]
[Trait("Category", "Integration.Web")] [Collection("API")]
public class CarsApiSpec : WebApiSpecSetup<Program>
{
public CarsApiSpec(WebApplicationFactory<Program> factory) : base(factory)
Expand Down Expand Up @@ -354,7 +354,7 @@ From that Application layer, a resource (DTO) will be returned, and this functio
[AuthorizeForAnyRole(OrganizationRoles.Reserver, OrganizationRoles.Manager)]
public async Task<ApiGetResult<Car, GetCarResponse>> Get(GetCarRequest request, CancellationToken cancellationToken)
{
var car = await _carsApplication.GetCarAsync(_context, request.Id, cancellationToken);
var car = await _carsApplication.GetCarAsync(_contextFactory.Create(), request.Id, cancellationToken);

return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c });
}
Expand Down
15 changes: 15 additions & 0 deletions docs/design-principles/0090-authentication-authorization.md.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Authentication & Authorization

## Design Principles

## Implementation

Cookie Authentication

Usually performed by a BackendForFrontend component, reverse-proxies the token hidden in the cookie, into a token passed to the backend

Authorization

For marked endpoints, verifies that the cookie exists.


Binary file modified docs/images/Physical-Architecture-AWS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Physical-Architecture-Azure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/Recorder-AWS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Sources.pptx
Binary file not shown.
Binary file modified docs/images/Subdomains.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/AWSLambdas.Api.WorkerHost/HostExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using Application.Interfaces.Services;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using Common;
using Common.Configuration;
using Common.Recording;
Expand Down Expand Up @@ -38,6 +39,7 @@ public static void AddDependencies(this IServiceCollection services, IConfigurat
c.Resolve<ICrashReporter>()));
services.AddSingleton<IServiceClient>(c =>
new InterHostServiceClient(c.Resolve<IHttpClientFactory>(),
c.Resolve<JsonSerializerOptions>(),
c.Resolve<IHostSettings>().GetAncillaryApiHostBaseUrl()));
services.AddSingleton<IQueueMonitoringApiRelayWorker<UsageMessage>, DeliverUsageRelayWorker>();
services.AddSingleton<IQueueMonitoringApiRelayWorker<AuditMessage>, DeliverAuditRelayWorker>();
Expand Down
2 changes: 1 addition & 1 deletion src/AWSLambdas.Api.WorkerHost/Lambdas/DeliverAudit.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using AWSLambdas.Api.WorkerHost.Extensions;
using Infrastructure.Workers.Api;

Expand Down
2 changes: 1 addition & 1 deletion src/AWSLambdas.Api.WorkerHost/Lambdas/DeliverUsage.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using AWSLambdas.Api.WorkerHost.Extensions;
using Infrastructure.Workers.Api;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using AncillaryDomain;
using Application.Interfaces;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using Common;
using Common.Extensions;
using Domain.Common.Identity;
Expand Down
1 change: 1 addition & 0 deletions src/AncillaryApplication/AncillaryApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Application.Interfaces;
using Application.Persistence.Interfaces;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using Application.Resources.Shared;
using Common;
using Common.Extensions;
Expand Down
8 changes: 5 additions & 3 deletions src/AncillaryInfrastructure.IntegrationTests/AuditsApiSpec.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AncillaryInfrastructure.IntegrationTests.Stubs;
using ApiHost1;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using Common;
using Common.Extensions;
using FluentAssertions;
Expand All @@ -14,6 +15,7 @@
namespace AncillaryInfrastructure.IntegrationTests;

[Trait("Category", "Integration.Web")]
[Collection("API")]
public class AuditsApiSpec : WebApiSpec<Program>
{
private readonly IAuditMessageQueueRepository _auditMessageQueue;
Expand Down Expand Up @@ -42,7 +44,7 @@ public async Task WhenDeliverAudit_ThenDelivers()
Arguments = new List<string> { "anarg1", "anarg2" }
}.ToJson()!
};
var result = await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
var result = await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

result.Content.Value.IsDelivered.Should().BeTrue();

Expand All @@ -66,7 +68,7 @@ public async Task WhenDeliverAudit_ThenDelivers()
public async Task WhenDrainAllAuditsAndNone_ThenDoesNotDrainAny()
{
var request = new DrainAllAuditsRequest();
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

var audits = await Api.GetAsync(new SearchAllAuditsRequest());

Expand Down Expand Up @@ -105,7 +107,7 @@ public async Task WhenDrainAllAuditsAndSome_ThenDrains()
}, CancellationToken.None);

var request = new DrainAllAuditsRequest();
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

var audits = await Api.GetAsync(new SearchAllAuditsRequest
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace AncillaryInfrastructure.IntegrationTests;

[Trait("Category", "Integration.Web")]
[Collection("API")]
public class RecordingApiSpec : WebApiSpec<Program>
{
private readonly StubRecorder _recorder;
Expand All @@ -32,7 +33,7 @@ public async Task WhenRecordUseWithNoAdditional_ThenRecords()
EventName = "aneventname",
Additional = null
};
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

_recorder.LastUsageEventName.Should().Be("aneventname");
_recorder.LastUsageAdditional.Should().BeNull();
Expand All @@ -51,7 +52,7 @@ public async Task WhenRecordUse_ThenRecords()
{ "aname3", true }
}
};
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

_recorder.LastUsageEventName.Should().Be("aneventname");
_recorder.LastUsageAdditional!.Count.Should().Be(3);
Expand All @@ -73,7 +74,7 @@ public async Task WhenRecordMeasure_ThenRecords()
{ "aname3", true }
}
};
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

_recorder.LastMeasureEventName.Should().Be("aneventname");
_recorder.LastMeasureAdditional!.Count.Should().Be(3);
Expand Down
8 changes: 5 additions & 3 deletions src/AncillaryInfrastructure.IntegrationTests/UsagesApiSpec.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AncillaryInfrastructure.IntegrationTests.Stubs;
using ApiHost1;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using Common;
using Common.Extensions;
using FluentAssertions;
Expand All @@ -15,6 +16,7 @@
namespace AncillaryInfrastructure.IntegrationTests;

[Trait("Category", "Integration.Web")]
[Collection("API")]
public class UsagesApiSpec : WebApiSpec<Program>
{
private readonly IUsageMessageQueueRepository _usageMessageQueue;
Expand Down Expand Up @@ -43,7 +45,7 @@ public async Task WhenDeliverUsage_ThenDelivers()
MessageId = "amessageid"
}.ToJson()!
};
var result = await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
var result = await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

result.Content.Value.IsDelivered.Should().BeTrue();
_usageReportingService.LastEventName.Should().Be("aneventname");
Expand All @@ -54,7 +56,7 @@ public async Task WhenDeliverUsage_ThenDelivers()
public async Task WhenDrainAllUsagesAndNone_ThenDoesNotDrainAny()
{
var request = new DrainAllUsagesRequest();
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

_usageReportingService.LastEventName.Should().BeNone();
}
Expand Down Expand Up @@ -85,7 +87,7 @@ public async Task WhenDrainAllUsagesAndSome_ThenDrains()
}, CancellationToken.None);

var request = new DrainAllUsagesRequest();
await Api.PostAsync(request, req => req.SetHmacAuth(request, "asecret"));
await Api.PostAsync(request, req => req.SetHMACAuth(request, "asecret"));

_usageReportingService.AllEventNames.Count.Should().Be(3);
_usageReportingService.AllEventNames.Should().ContainInOrder("aneventname1", "aneventname2", "aneventname3");
Expand Down
1 change: 1 addition & 0 deletions src/AncillaryInfrastructure/AncillaryInfrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\AncillaryApplication\AncillaryApplication.csproj" />
<ProjectReference Include="..\Application.Persistence.Shared\Application.Persistence.Shared.csproj" />
<ProjectReference Include="..\Infrastructure.Shared\Infrastructure.Shared.csproj" />
<ProjectReference Include="..\Infrastructure.Web.Api.Common\Infrastructure.Web.Api.Common.csproj" />
<ProjectReference Include="..\Infrastructure.Web.Api.Interfaces\Infrastructure.Web.Api.Interfaces.csproj" />
<ProjectReference Include="..\Infrastructure.Web.Api.Operations.Shared\Infrastructure.Web.Api.Operations.Shared.csproj" />
Expand Down
Loading

0 comments on commit 03fd6b9

Please sign in to comment.