Skip to content

Commit

Permalink
Custom auth (#238)
Browse files Browse the repository at this point in the history
* Indented

* Update packages again.

* Update libs again.

* Custom auth.

* Custom auth settings.

* Tests more stable.
  • Loading branch information
SebastianStehle committed Apr 29, 2024
1 parent 814e5bf commit f503a13
Show file tree
Hide file tree
Showing 139 changed files with 3,008 additions and 1,591 deletions.
2 changes: 2 additions & 0 deletions backend/src/Notifo.Domain/Apps/App.cs
Expand Up @@ -26,6 +26,8 @@ public sealed record App(string Id, Instant Created)

public Instant LastUpdate { get; init; }

public AppAuthScheme? AuthScheme { get; init; }

public ReadonlyList<string> Languages { get; init; } = DefaultLanguages;

public ReadonlyDictionary<string, string> ApiKeys { get; init; } = ReadonlyDictionary.Empty<string, string>();
Expand Down
23 changes: 23 additions & 0 deletions backend/src/Notifo.Domain/Apps/AppAuthScheme.cs
@@ -0,0 +1,23 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

namespace Notifo.Domain.Apps;

public sealed class AppAuthScheme
{
public string Domain { get; init; }

public string DisplayName { get; init; }

public string ClientId { get; init; }

public string ClientSecret { get; init; }

public string Authority { get; init; }

public string? SignoutRedirectUrl { get; init; }
}
18 changes: 18 additions & 0 deletions backend/src/Notifo.Domain/Apps/AppStore.cs
Expand Up @@ -45,6 +45,12 @@ public void Dispose()
}
}

public Task<bool> AnyAuthDomainAsync(
CancellationToken ct = default)
{
return repository.AnyAuthDomainAsync(ct);
}

public IAsyncEnumerable<App> QueryAllAsync(
CancellationToken ct = default)
{
Expand Down Expand Up @@ -116,6 +122,18 @@ public void Dispose()
return app;
}

public async Task<App?> GetByAuthDomainAsync(string domain,
CancellationToken ct = default)
{
Guard.NotNullOrEmpty(domain);

var (app, _) = await repository.GetByAuthDomainAsync(domain, ct);

await DeliverAsync(app);

return app;
}

public async ValueTask<App?> HandleAsync(AppCommand command,
CancellationToken ct)
{
Expand Down
19 changes: 19 additions & 0 deletions backend/src/Notifo.Domain/Apps/DeleteAppAuthScheme.cs
@@ -0,0 +1,19 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

namespace Notifo.Domain.Apps;

public sealed class DeleteAppAuthScheme : AppCommand
{
public override ValueTask<App?> ExecuteAsync(App target, IServiceProvider serviceProvider,
CancellationToken ct)
{
target = target with { AuthScheme = null };

return new ValueTask<App?>(target);
}
}
6 changes: 6 additions & 0 deletions backend/src/Notifo.Domain/Apps/IAppRepository.cs
Expand Up @@ -26,9 +26,15 @@ public interface IAppRepository : ICounterStore<string>
Task<(App? App, string? Etag)> GetAsync(string id,
CancellationToken ct = default);

Task<(App? App, string? Etag)> GetByAuthDomainAsync(string domain,
CancellationToken ct = default);

Task UpsertAsync(App app, string? oldEtag = null,
CancellationToken ct = default);

Task DeleteAsync(string id,
CancellationToken ct = default);

Task<bool> AnyAuthDomainAsync(
CancellationToken ct = default);
}
6 changes: 6 additions & 0 deletions backend/src/Notifo.Domain/Apps/IAppStore.cs
Expand Up @@ -24,6 +24,12 @@ public interface IAppStore
Task<App?> GetAsync(string id,
CancellationToken ct = default);

Task<App?> GetByAuthDomainAsync(string domain,
CancellationToken ct = default);

Task<App?> GetCachedAsync(string id,
CancellationToken ct = default);

Task<bool> AnyAuthDomainAsync(
CancellationToken ct = default);
}
22 changes: 22 additions & 0 deletions backend/src/Notifo.Domain/Apps/MongoDb/MongoDbAppRepository.cs
Expand Up @@ -109,6 +109,19 @@ await Collection.Find(x => x.ContributorIds.Contains(contributorId))
}
}

public async Task<(App? App, string? Etag)> GetByAuthDomainAsync(string domain,
CancellationToken ct = default)
{
using (Telemetry.Activities.StartActivity("MongoDbAppRepository/GetByAuthDomainAsync"))
{
var document = await
Collection.Find(x => x.Doc.AuthScheme!.Domain == domain)
.FirstOrDefaultAsync(ct);

return (document?.ToApp(), document?.Etag);
}
}

public async Task<(App? App, string? Etag)> GetAsync(string id,
CancellationToken ct = default)
{
Expand All @@ -131,6 +144,15 @@ await Collection.Find(x => x.ContributorIds.Contains(contributorId))
}
}

public async Task<bool> AnyAuthDomainAsync(
CancellationToken ct = default)
{
using (Telemetry.Activities.StartActivity("MongoDbAppRepository/AnyAuthDomainAsync"))
{
return await Collection.Find(x => x.Doc.AuthScheme != null).AnyAsync(ct);
}
}

public async Task BatchWriteAsync(List<(string Key, CounterMap Counters)> counters,
CancellationToken ct)
{
Expand Down
62 changes: 62 additions & 0 deletions backend/src/Notifo.Domain/Apps/UpsertAppAuthScheme.cs
@@ -0,0 +1,62 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using FluentValidation;
using Notifo.Infrastructure.Validation;

namespace Notifo.Domain.Apps;

public sealed class UpsertAppAuthScheme : AppCommand
{
public string Domain { get; init; }

public string DisplayName { get; init; }

public string ClientId { get; init; }

public string ClientSecret { get; init; }

public string Authority { get; init; }

public string? SignoutRedirectUrl { get; init; }

private sealed class Validator : AbstractValidator<UpsertAppAuthScheme>
{
public Validator()
{
RuleFor(x => x.Domain).NotNull().NotEmpty().Domain();
RuleFor(x => x.DisplayName).NotNull().NotEmpty();
RuleFor(x => x.ClientId).NotNull().NotEmpty();
RuleFor(x => x.ClientSecret).NotNull().NotEmpty();
RuleFor(x => x.Authority).NotNull().NotEmpty().Url();
RuleFor(x => x.SignoutRedirectUrl).Url();
}
}

public override ValueTask<App?> ExecuteAsync(App target, IServiceProvider serviceProvider,
CancellationToken ct)
{
Validate<Validator>.It(this);

var newScheme = new AppAuthScheme
{
Authority = Authority,
ClientId = ClientId,
ClientSecret = ClientSecret,
DisplayName = DisplayName,
Domain = Domain,
SignoutRedirectUrl = SignoutRedirectUrl,
};

if (!Equals(target.AuthScheme, newScheme))
{
target = target with { AuthScheme = newScheme };
}

return new ValueTask<App?>(target);
}
}
9 changes: 9 additions & 0 deletions backend/src/Notifo.Domain/Resources/Texts.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions backend/src/Notifo.Domain/Resources/Texts.resx
Expand Up @@ -186,6 +186,9 @@
<data name="TemplateError" xml:space="preserve">
<value>Unhandled template error.</value>
</data>
<data name="ValidationDomain" xml:space="preserve">
<value>{PropertyName} must be a valid domain.</value>
</data>
<data name="ValidationLanguage" xml:space="preserve">
<value>{PropertyName} must be a valid language.</value>
</data>
Expand Down
Expand Up @@ -33,6 +33,14 @@ public static class ValidatorExtensions
}).WithMessage(Texts.ValidationUrl);
}

public static IRuleBuilderOptions<T, string?> Domain<T>(this IRuleBuilder<T, string?> ruleBuilder)
{
return ruleBuilder.Must(value =>
{
return string.IsNullOrWhiteSpace(value) || Uri.CheckHostName(value) == UriHostNameType.Dns;
}).WithMessage(Texts.ValidationDomain);
}

public static IRuleBuilderOptions<T, string?> Language<T>(this IRuleBuilder<T, string?> ruleBuilder)
{
return ruleBuilder.Must(value =>
Expand Down
Expand Up @@ -51,7 +51,11 @@ public static AuthenticationBuilder AddOidc(this AuthenticationBuilder authBuild

authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options =>
{
options.Events = new OidcHandler(identityOptions);
options.Events = new OidcHandler(new OdicOptions
{
SignoutRedirectUrl = identityOptions.OidcOnSignoutRedirectUrl
});
options.Authority = identityOptions.OidcAuthority;
options.ClientId = identityOptions.OidcClient;
options.ClientSecret = identityOptions.OidcSecret;
Expand Down
21 changes: 21 additions & 0 deletions backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectHandler.cs
@@ -0,0 +1,21 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Notifo.Identity.Dynamic;

public sealed class DynamicOpenIdConnectHandler : OpenIdConnectHandler
{
public DynamicOpenIdConnectHandler(IOptionsMonitor<DynamicOpenIdConnectOptions> options, ILoggerFactory logger, HtmlEncoder htmlEncoder, UrlEncoder encoder)
: base(options, logger, htmlEncoder, encoder)
{
}
}
14 changes: 14 additions & 0 deletions backend/src/Notifo.Identity/Dynamic/DynamicOpenIdConnectOptions.cs
@@ -0,0 +1,14 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

namespace Notifo.Identity.Dynamic;

public sealed class DynamicOpenIdConnectOptions : OpenIdConnectOptions
{
}

0 comments on commit f503a13

Please sign in to comment.