Skip to content

Commit

Permalink
Midway through adding PasswordCredential authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jan 5, 2024
1 parent ddf8f22 commit 46c040a
Show file tree
Hide file tree
Showing 250 changed files with 10,035 additions and 396 deletions.
57 changes: 57 additions & 0 deletions docs/decisions/0100-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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 include usernames+passwords, pin numbers etc). Once that proof is verified, the end-user can be identified, and information about the end-user can be used downstream to authorize the user and apply rules to their use of the software.

Today there are many options for integrating AuthN into a SaaS product, 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 easily (at low cost),
* Offer reasonable flexibility (to expand to changing needs of a particular business).
* The chosen solution must be customized easily, and can be easily upgraded or replaced later with other integrations.
* 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).
* We prefer to utilize refreshable, transparent, signed JWT tokens.
* 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 user credentials solution out of the box.

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`

- No additional operational costs, (unlike IdentityServer, Auth0, etc)
- Can be swapped out for an implementation of IdentityServer, Auth0, Okta, or other solution.
- Has decent support for most of the most common capabilities for 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 are not limited to opaque non-JWT tokens (as they are in ANCI), and we can control the behaviour of each of the APIs, which cannot be done in ANCI.

## 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-)
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.
2 changes: 1 addition & 1 deletion src/AWSLambdas.Api.WorkerHost/HostExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Application.Interfaces.Services;
using Application.Persistence.Shared;
using Application.Persistence.Shared.ReadModels;
using Common;
using Common.Configuration;
using Common.Recording;
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
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 Down
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 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
14 changes: 8 additions & 6 deletions src/AncillaryInfrastructure/AncillaryModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@ public class AncillaryModule : ISubDomainModule
{ typeof(AuditRoot), "audit" }
};

public Action<WebApplication> MinimalApiRegistrationFunction
{
get { return app => app.RegisterRoutes(); }
}

public Action<ConfigurationManager, IServiceCollection> RegisterServicesFunction
public Action<ConfigurationManager, IServiceCollection> RegisterServices
{
get
{
Expand All @@ -48,6 +43,8 @@ public Action<ConfigurationManager, IServiceCollection> RegisterServicesFunction
new UsageMessageQueueRepository(c.Resolve<IRecorder>(), c.ResolveForPlatform<IQueueStore>()));
services.RegisterUnshared<IAuditMessageQueueRepository>(c =>
new AuditMessageQueueRepository(c.Resolve<IRecorder>(), c.ResolveForPlatform<IQueueStore>()));
services.RegisterUnshared<IEmailMessageQueueRepository>(c =>
new EmailMessageQueueRepository(c.Resolve<IRecorder>(), c.ResolveForPlatform<IQueueStore>()));
services.RegisterUnshared<IAuditRepository>(c => new AuditRepository(c.ResolveForUnshared<IRecorder>(),
c.ResolveForUnshared<IDomainFactory>(),
c.ResolveForUnshared<IEventSourcingDddCommandStore<AuditRoot>>(),
Expand All @@ -60,4 +57,9 @@ public Action<ConfigurationManager, IServiceCollection> RegisterServicesFunction
};
}
}

public Action<WebApplication> ConfigureMiddleware
{
get { return app => app.RegisterRoutes(); }
}
}
9 changes: 5 additions & 4 deletions src/AncillaryInfrastructure/Persistence/AuditRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,16 @@ public async Task<Result<AuditRoot, Error>> SaveAsync(AuditRoot audit, Cancellat
public async Task<Result<IReadOnlyList<Audit>, Error>> SearchAllAsync(Identifier organizationId,
SearchOptions searchOptions, CancellationToken cancellationToken)
{
var audits = await _auditQueries.QueryAsync(Query.From<Audit>()
var queried = await _auditQueries.QueryAsync(Query.From<Audit>()
.Where<string>(u => u.OrganizationId, ConditionOperator.EqualTo, organizationId)
.WithSearchOptions(searchOptions), cancellationToken: cancellationToken);
if (!audits.IsSuccessful)
if (!queried.IsSuccessful)
{
return audits.Error;
return queried.Error;
}

return audits.Value.Results;
var audits = queried.Value.Results;
return audits;
}

public async Task<Result<AuditRoot, Error>> LoadAsync(Identifier organizationId, Identifier id,
Expand Down
17 changes: 17 additions & 0 deletions src/ApiHost1/Api/TestingOnly/TestingWebApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ namespace ApiHost1.Api.TestingOnly;

public sealed class TestingWebApi : IWebApiService
{
// ReSharper disable once InconsistentNaming
public async Task<ApiResult<string, StringMessageTestingOnlyResponse>> AuthNHMAC(
AuthNHMACTestingOnlyRequest request, CancellationToken cancellationToken)
{
await Task.CompletedTask;
return () => new Result<StringMessageTestingOnlyResponse, Error>(new StringMessageTestingOnlyResponse
{ Message = "amessage" });
}

public async Task<ApiResult<string, StringMessageTestingOnlyResponse>> AuthNToken(
AuthNTokenTestingOnlyRequest request, CancellationToken cancellationToken)
{
await Task.CompletedTask;
return () => new Result<StringMessageTestingOnlyResponse, Error>(new StringMessageTestingOnlyResponse
{ Message = "amessage" });
}

public async Task<ApiResult<string, StringMessageTestingOnlyResponse>> ContentNegotiationGet(
ContentNegotiationsTestingOnlyRequest request, CancellationToken cancellationToken)
{
Expand Down
2 changes: 2 additions & 0 deletions src/ApiHost1/ApiHost1.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

<ItemGroup>
<ProjectReference Include="..\AncillaryInfrastructure\AncillaryInfrastructure.csproj" />
<ProjectReference Include="..\EndUsersInfrastructure\EndUsersInfrastructure.csproj" />
<ProjectReference Include="..\IdentityInfrastructure\IdentityInfrastructure.csproj" />
<ProjectReference Include="..\BookingsInfrastructure\BookingsInfrastructure.csproj" />
<ProjectReference Include="..\CarsInfrastructure\CarsInfrastructure.csproj" />
<ProjectReference Include="..\Infrastructure.Web.Hosting.Common\Infrastructure.Web.Hosting.Common.csproj" />
Expand Down
47 changes: 47 additions & 0 deletions src/ApiHost1/ApiHostModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Reflection;
using AncillaryInfrastructure.Api.Usages;
using Application.Persistence.Shared;
using Application.Services.Shared;
using Common;
using Domain.Services.Shared.DomainServices;
using Infrastructure.Hosting.Common.Extensions;
using Infrastructure.Persistence.Interfaces;
using Infrastructure.Persistence.Shared.ApplicationServices;
using Infrastructure.Shared.ApplicationServices;
using Infrastructure.Web.Hosting.Common;

namespace ApiHost1;

/// <summary>
/// Provides a module for common services of a API host
/// </summary>
public class ApiHostModule : ISubDomainModule
{
public Assembly ApiAssembly => typeof(UsagesApi).Assembly;

public Assembly DomainAssembly => null!;

public Dictionary<Type, string> AggregatePrefixes => new();

public Action<ConfigurationManager, IServiceCollection> RegisterServices
{
get
{
return (_, services) =>
{
services.RegisterUnshared<IEmailMessageQueueRepository>(c =>
new EmailMessageQueueRepository(c.Resolve<IRecorder>(), c.ResolveForPlatform<IQueueStore>()));
services.RegisterUnshared<ITokensService, TokensService>();
services.RegisterUnshared<INotificationsService, EmailNotificationsService>();
services.RegisterUnshared<IWebsiteUiService, WebsiteUiService>();
services.RegisterUnshared<IEmailQueuingService, EmailQueuingService>();
};
}
}

public Action<WebApplication> ConfigureMiddleware
{
get { return _ => { }; }
}
}
5 changes: 5 additions & 0 deletions src/ApiHost1/HostedModules.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using AncillaryInfrastructure;
using BookingsInfrastructure;
using CarsInfrastructure;
using EndUsersInfrastructure;
using IdentityInfrastructure;
using Infrastructure.Web.Hosting.Common;

namespace ApiHost1;
Expand All @@ -10,6 +12,9 @@ public static class HostedModules
public static SubDomainModules Get()
{
var modules = new SubDomainModules();
modules.Register(new ApiHostModule());
modules.Register(new EndUsersModule());
modules.Register(new IdentityModule());
modules.Register(new AncillaryModule());
#if TESTINGONLY
modules.Register(new TestingOnlyApiModule());
Expand Down
10 changes: 5 additions & 5 deletions src/ApiHost1/TestingOnlyApiModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ public class TestingOnlyApiModule : ISubDomainModule
{
public Assembly ApiAssembly => typeof(TestingWebApi).Assembly;

public Assembly DomainAssembly => typeof(TestingWebApi).Assembly;
public Assembly DomainAssembly => null!;

public Dictionary<Type, string> AggregatePrefixes => new();

public Action<WebApplication> MinimalApiRegistrationFunction
public Action<ConfigurationManager, IServiceCollection> RegisterServices
{
get { return app => app.RegisterRoutes(); }
get { return (_, _) => { }; }
}

public Action<ConfigurationManager, IServiceCollection> RegisterServicesFunction
public Action<WebApplication> ConfigureMiddleware
{
get { return (_, _) => { }; }
get { return app => app.RegisterRoutes(); }
}
}
#endif
16 changes: 16 additions & 0 deletions src/ApiHost1/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,29 @@
"LocalMachineJsonFileStore": {
"RootPath": "./saastack/local"
}
},
"Notifications": {
"SenderProductName": "SaaStack",
"SenderEmailAddress": "[email protected]",
"SenderDisplayName": "Support"
}
},
"Hosts": {
"AncillaryApi": {
"BaseUrl": "https://localhost:5001",
"HmacAuthNSecret": "asecret"
},
"IdentityApi": {
"BaseUrl": "https://localhost:5001",
"PasswordCredential": {
"CooldownPeriodInMinutes": 5,
"MaxFailedLogins": 5
},
"JWT": {
"SigningSecret": "asecretsigningkey",
"DefaultExpiryInMinutes": 15
}
},
"WebsiteHost": {
"BaseUrl": "https://localhost:5101"
}
Expand Down
Loading

0 comments on commit 46c040a

Please sign in to comment.