Skip to content

Commit

Permalink
Custom auth (#239)
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.

* Test the auth.

* Cleanup

* Small fixes.
  • Loading branch information
SebastianStehle authored May 8, 2024
1 parent f503a13 commit 61e02d1
Show file tree
Hide file tree
Showing 34 changed files with 409 additions and 307 deletions.
37 changes: 14 additions & 23 deletions backend/src/Notifo.Domain/Apps/UpsertAppAuthScheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ 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; }
public AppAuthScheme? Scheme { get; set; }

private sealed class Validator : AbstractValidator<UpsertAppAuthScheme>
{
public Validator()
{
When(x => x.Scheme != null, () =>
{
RuleFor(x => x.Scheme).SetValidator(new SchemeValidator()!);
});
}
}

private sealed class SchemeValidator : AbstractValidator<AppAuthScheme>
{
public SchemeValidator()
{
RuleFor(x => x.Domain).NotNull().NotEmpty().Domain();
RuleFor(x => x.DisplayName).NotNull().NotEmpty();
Expand All @@ -42,19 +43,9 @@ public Validator()
{
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))
if (!Equals(target.AuthScheme, Scheme))
{
target = target with { AuthScheme = newScheme };
target = target with { AuthScheme = Scheme };
}

return new ValueTask<App?>(target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static AuthenticationBuilder AddOidc(this AuthenticationBuilder authBuild

authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options =>
{
options.Events = new OidcHandler(new OdicOptions
options.Events = new OidcHandler(new OidcOptions
{
SignoutRedirectUrl = identityOptions.OidcOnSignoutRedirectUrl
});
Expand Down
78 changes: 55 additions & 23 deletions backend/src/Notifo.Identity/Dynamic/DynamicSchemeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
using Microsoft.Extensions.Options;
using Notifo.Domain.Apps;

#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter

namespace Notifo.Identity.Dynamic;

public sealed class DynamicSchemeProvider : AuthenticationSchemeProvider, IOptionsMonitor<DynamicOpenIdConnectOptions>
Expand All @@ -19,23 +22,34 @@ public sealed class DynamicSchemeProvider : AuthenticationSchemeProvider, IOptio

private readonly IAppStore appStore;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IConfigurationStore<AppAuthScheme> temporarySchemes;
private readonly OpenIdConnectPostConfigureOptions configure;

public DynamicOpenIdConnectOptions CurrentValue => null!;

public DynamicSchemeProvider(IAppStore appStore, IHttpContextAccessor httpContextAccessor,
private sealed record SchemeResult(AuthenticationScheme Scheme, DynamicOpenIdConnectOptions Options);

public DynamicSchemeProvider(
IAppStore appStore,
IHttpContextAccessor httpContextAccessor,
IConfigurationStore<AppAuthScheme> temporarySchemes,
OpenIdConnectPostConfigureOptions configure,
IOptions<AuthenticationOptions> options)
: base(options)
{
this.appStore = appStore;
this.httpContextAccessor = httpContextAccessor;
this.temporarySchemes = temporarySchemes;
this.configure = configure;
}

public Task<bool> HasCustomSchemeAsync()
public async Task<string> AddTemporarySchemeAsync(AppAuthScheme scheme,
CancellationToken ct = default)
{
return appStore.AnyAuthDomainAsync(default);
var id = Guid.NewGuid().ToString();

await temporarySchemes.SetAsync(id, scheme, TimeSpan.FromMinutes(10), ct);
return id;
}

public async Task<AuthenticationScheme?> GetSchemaByEmailAddressAsync(string email)
Expand Down Expand Up @@ -64,7 +78,7 @@ public Task<bool> HasCustomSchemeAsync()

public override async Task<AuthenticationScheme?> GetSchemeAsync(string name)
{
var result = await GetSchemeCoreAsync(name);
var result = await GetSchemeCoreAsync(name, default);

if (result != null)
{
Expand Down Expand Up @@ -98,7 +112,8 @@ public override async Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerS
{
var name = lastSegment[prefix.Length..];

var scheme = await GetSchemeCoreAsync(name);
var scheme = await GetSchemeCoreAsync(name, httpContextAccessor.HttpContext.RequestAborted);

if (scheme != null)
{
result.Add(scheme.Scheme);
Expand All @@ -116,32 +131,34 @@ public DynamicOpenIdConnectOptions Get(string? name)
return new DynamicOpenIdConnectOptions();
}

var scheme = GetSchemeCoreAsync(name).Result;
var scheme = GetSchemeCoreAsync(name, default).Result;

return scheme?.Options ?? new DynamicOpenIdConnectOptions();
}

public IDisposable? OnChange(Action<DynamicOpenIdConnectOptions, string?> listener)
private async Task<SchemeResult?> GetSchemeCoreAsync(string name,
CancellationToken ct)
{
return null;
}
if (!Guid.TryParse(name, out _))
{
return null;
}

private async Task<SchemeResult?> GetSchemeCoreAsync(string name)
{
var cacheKey = ("DYNAMIC_SCHEME", name);

if (httpContextAccessor.HttpContext?.Items.TryGetValue(cacheKey, out var cached) == true)
{
return cached as SchemeResult;
}

var app = await appStore.GetAsync(name, default);
var scheme =
await GetSchemeByAppAsync(name, ct) ??
await GetSchemeByTempNameAsync(name, ct);

var result = (SchemeResult?)null;
if (app?.AuthScheme != null)
{
result = CreateScheme(app.Id, app.AuthScheme);
}
var result =
scheme != null ?
CreateScheme(name, scheme) :
null;

if (httpContextAccessor.HttpContext != null)
{
Expand All @@ -151,13 +168,29 @@ public DynamicOpenIdConnectOptions Get(string? name)
return result;
}

private async Task<AppAuthScheme?> GetSchemeByAppAsync(string name,
CancellationToken ct)
{
var app = await appStore.GetByAuthDomainAsync(name, ct);

return app?.AuthScheme;
}

private async Task<AppAuthScheme?> GetSchemeByTempNameAsync(string name,
CancellationToken ct)
{
var scheme = await temporarySchemes.GetAsync(name, ct);

return scheme;
}

private SchemeResult CreateScheme(string name, AppAuthScheme config)
{
var scheme = new AuthenticationScheme(name, config.DisplayName, typeof(DynamicOpenIdConnectHandler));

var options = new DynamicOpenIdConnectOptions
{
Events = new OidcHandler(new OdicOptions
Events = new OidcHandler(new OidcOptions
{
SignoutRedirectUrl = config.SignoutRedirectUrl
}),
Expand All @@ -176,9 +209,8 @@ private SchemeResult CreateScheme(string name, AppAuthScheme config)
return new SchemeResult(scheme, options);
}

#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
private sealed record SchemeResult(AuthenticationScheme Scheme, DynamicOpenIdConnectOptions Options);
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
#pragma warning restore RECS0082 // Parameter has the same name as a member and hides it
public IDisposable? OnChange(Action<DynamicOpenIdConnectOptions, string?> listener)
{
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

namespace Notifo.Domain.Apps;
namespace Notifo.Identity.Dynamic;

public sealed class DeleteAppAuthScheme : AppCommand
public interface IConfigurationStore<T> where T : class
{
public override ValueTask<App?> ExecuteAsync(App target, IServiceProvider serviceProvider,
CancellationToken ct)
{
target = target with { AuthScheme = null };
Task SetAsync(string key, T value, TimeSpan ttl,
CancellationToken ct = default);

return new ValueTask<App?>(target);
}
Task<T?> GetAsync(string key,
CancellationToken ct = default);
}
4 changes: 4 additions & 0 deletions backend/src/Notifo.Identity/IdentityServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Logging;
using Notifo.Domain.Apps;
using Notifo.Domain.Identity;
using Notifo.Identity;
using Notifo.Identity.ApiKey;
Expand Down Expand Up @@ -153,6 +154,9 @@ public static void AddMyMongoDbIdentity(this IServiceCollection services)
services.AddSingletonAs<MongoDbXmlRepository>()
.As<IXmlRepository>();

services.AddSingletonAs<MongoDbConfigurationStore<AppAuthScheme>>()
.As<IConfigurationStore<AppAuthScheme>>();

services.ConfigureOptions<MongoDbKeyOptions>();

services.Configure<KeyManagementOptions>((c, options) =>
Expand Down
17 changes: 17 additions & 0 deletions backend/src/Notifo.Identity/MongoDb/MongoDbConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

namespace Notifo.Identity.MongoDb;

public sealed class MongoDbConfiguration<T>
{
public string Id { get; set; }

public T Value { get; set; }

public DateTime Expires { get; set; }
}
61 changes: 61 additions & 0 deletions backend/src/Notifo.Identity/MongoDb/MongoDbConfigurationStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// ==========================================================================
// Notifo.io
// ==========================================================================
// Copyright (c) Sebastian Stehle
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using MongoDB.Driver;
using Notifo.Identity.Dynamic;
using Notifo.Infrastructure.MongoDb;

namespace Notifo.Identity.MongoDb;

public sealed class MongoDbConfigurationStore<T> : MongoDbRepository<MongoDbConfiguration<T>>, IConfigurationStore<T> where T : class
{
public MongoDbConfigurationStore(IMongoDatabase database)
: base(database)
{
}

protected override string CollectionName()
{
return "Identity_Configuration";
}

protected override Task SetupCollectionAsync(IMongoCollection<MongoDbConfiguration<T>> collection,
CancellationToken ct = default)
{
return collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoDbConfiguration<T>>(
IndexKeys.Ascending(x => x.Expires),
new CreateIndexOptions
{
ExpireAfter = TimeSpan.Zero
}),
cancellationToken: ct);
}

public async Task<T?> GetAsync(string key,
CancellationToken ct = default)
{
var entity = await Collection.Find(x => x.Id == key).FirstOrDefaultAsync(ct);

return entity?.Value;
}

public async Task SetAsync(string key, T value, TimeSpan ttl,
CancellationToken ct = default)
{
var expires = DateTime.UtcNow + ttl;

await Collection.UpdateOneAsync(
Filter.Eq(x => x.Id, key),
Update
.SetOnInsert(x => x.Id, key)
.Set(x => x.Value, value)
.Set(x => x.Expires, expires),
Upsert,
cancellationToken: ct);
}
}
2 changes: 2 additions & 0 deletions backend/src/Notifo.Identity/NotifoIdentityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public sealed class NotifoIdentityOptions
{
public bool AllowPasswordAuth { get; set; }

public bool AllowCustomAuth { get; set; }

public string AdminClientId { get; set; }

public string AdminClientSecret { get; set; }
Expand Down
6 changes: 3 additions & 3 deletions backend/src/Notifo.Identity/OidcHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@

namespace Notifo.Identity;

public class OdicOptions
public class OidcOptions
{
public string? SignoutRedirectUrl { get; set; }
}

public sealed class OidcHandler : OpenIdConnectEvents
{
private readonly OdicOptions options;
private readonly OidcOptions options;

public OidcHandler(OdicOptions options)
public OidcHandler(OidcOptions options)
{
this.options = options;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public AuthorizationController(IOpenIddictScopeManager scopeManager, IOpenIddict
}

[HttpPost("connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public class UserInfoController : ControllerBase<UserInfoController>
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("connect/userinfo")]
[HttpPost("connect/userinfo")]
[Produces("application/json")]
public async Task<IActionResult> Userinfo()
{
var user = await UserService.GetAsync(User, HttpContext.RequestAborted);
Expand Down
Loading

0 comments on commit 61e02d1

Please sign in to comment.