Skip to content

Commit 61e02d1

Browse files
Custom auth (#239)
* Indented * Update packages again. * Update libs again. * Custom auth. * Custom auth settings. * Tests more stable. * Test the auth. * Cleanup * Small fixes.
1 parent f503a13 commit 61e02d1

34 files changed

+409
-307
lines changed

backend/src/Notifo.Domain/Apps/UpsertAppAuthScheme.cs

+14-23
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,22 @@ namespace Notifo.Domain.Apps;
1212

1313
public sealed class UpsertAppAuthScheme : AppCommand
1414
{
15-
public string Domain { get; init; }
16-
17-
public string DisplayName { get; init; }
18-
19-
public string ClientId { get; init; }
20-
21-
public string ClientSecret { get; init; }
22-
23-
public string Authority { get; init; }
24-
25-
public string? SignoutRedirectUrl { get; init; }
15+
public AppAuthScheme? Scheme { get; set; }
2616

2717
private sealed class Validator : AbstractValidator<UpsertAppAuthScheme>
2818
{
2919
public Validator()
20+
{
21+
When(x => x.Scheme != null, () =>
22+
{
23+
RuleFor(x => x.Scheme).SetValidator(new SchemeValidator()!);
24+
});
25+
}
26+
}
27+
28+
private sealed class SchemeValidator : AbstractValidator<AppAuthScheme>
29+
{
30+
public SchemeValidator()
3031
{
3132
RuleFor(x => x.Domain).NotNull().NotEmpty().Domain();
3233
RuleFor(x => x.DisplayName).NotNull().NotEmpty();
@@ -42,19 +43,9 @@ public Validator()
4243
{
4344
Validate<Validator>.It(this);
4445

45-
var newScheme = new AppAuthScheme
46-
{
47-
Authority = Authority,
48-
ClientId = ClientId,
49-
ClientSecret = ClientSecret,
50-
DisplayName = DisplayName,
51-
Domain = Domain,
52-
SignoutRedirectUrl = SignoutRedirectUrl,
53-
};
54-
55-
if (!Equals(target.AuthScheme, newScheme))
46+
if (!Equals(target.AuthScheme, Scheme))
5647
{
57-
target = target with { AuthScheme = newScheme };
48+
target = target with { AuthScheme = Scheme };
5849
}
5950

6051
return new ValueTask<App?>(target);

backend/src/Notifo.Identity/AuthenticationBuilderExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static AuthenticationBuilder AddOidc(this AuthenticationBuilder authBuild
5151

5252
authBuilder.AddOpenIdConnect("ExternalOidc", displayName, options =>
5353
{
54-
options.Events = new OidcHandler(new OdicOptions
54+
options.Events = new OidcHandler(new OidcOptions
5555
{
5656
SignoutRedirectUrl = identityOptions.OidcOnSignoutRedirectUrl
5757
});

backend/src/Notifo.Identity/Dynamic/DynamicSchemeProvider.cs

+55-23
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
using Microsoft.Extensions.Options;
1212
using Notifo.Domain.Apps;
1313

14+
#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
15+
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
16+
1417
namespace Notifo.Identity.Dynamic;
1518

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

2023
private readonly IAppStore appStore;
2124
private readonly IHttpContextAccessor httpContextAccessor;
25+
private readonly IConfigurationStore<AppAuthScheme> temporarySchemes;
2226
private readonly OpenIdConnectPostConfigureOptions configure;
2327

2428
public DynamicOpenIdConnectOptions CurrentValue => null!;
2529

26-
public DynamicSchemeProvider(IAppStore appStore, IHttpContextAccessor httpContextAccessor,
30+
private sealed record SchemeResult(AuthenticationScheme Scheme, DynamicOpenIdConnectOptions Options);
31+
32+
public DynamicSchemeProvider(
33+
IAppStore appStore,
34+
IHttpContextAccessor httpContextAccessor,
35+
IConfigurationStore<AppAuthScheme> temporarySchemes,
2736
OpenIdConnectPostConfigureOptions configure,
2837
IOptions<AuthenticationOptions> options)
2938
: base(options)
3039
{
3140
this.appStore = appStore;
3241
this.httpContextAccessor = httpContextAccessor;
42+
this.temporarySchemes = temporarySchemes;
3343
this.configure = configure;
3444
}
3545

36-
public Task<bool> HasCustomSchemeAsync()
46+
public async Task<string> AddTemporarySchemeAsync(AppAuthScheme scheme,
47+
CancellationToken ct = default)
3748
{
38-
return appStore.AnyAuthDomainAsync(default);
49+
var id = Guid.NewGuid().ToString();
50+
51+
await temporarySchemes.SetAsync(id, scheme, TimeSpan.FromMinutes(10), ct);
52+
return id;
3953
}
4054

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

6579
public override async Task<AuthenticationScheme?> GetSchemeAsync(string name)
6680
{
67-
var result = await GetSchemeCoreAsync(name);
81+
var result = await GetSchemeCoreAsync(name, default);
6882

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

101-
var scheme = await GetSchemeCoreAsync(name);
115+
var scheme = await GetSchemeCoreAsync(name, httpContextAccessor.HttpContext.RequestAborted);
116+
102117
if (scheme != null)
103118
{
104119
result.Add(scheme.Scheme);
@@ -116,32 +131,34 @@ public DynamicOpenIdConnectOptions Get(string? name)
116131
return new DynamicOpenIdConnectOptions();
117132
}
118133

119-
var scheme = GetSchemeCoreAsync(name).Result;
134+
var scheme = GetSchemeCoreAsync(name, default).Result;
120135

121136
return scheme?.Options ?? new DynamicOpenIdConnectOptions();
122137
}
123138

124-
public IDisposable? OnChange(Action<DynamicOpenIdConnectOptions, string?> listener)
139+
private async Task<SchemeResult?> GetSchemeCoreAsync(string name,
140+
CancellationToken ct)
125141
{
126-
return null;
127-
}
142+
if (!Guid.TryParse(name, out _))
143+
{
144+
return null;
145+
}
128146

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

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

138-
var app = await appStore.GetAsync(name, default);
154+
var scheme =
155+
await GetSchemeByAppAsync(name, ct) ??
156+
await GetSchemeByTempNameAsync(name, ct);
139157

140-
var result = (SchemeResult?)null;
141-
if (app?.AuthScheme != null)
142-
{
143-
result = CreateScheme(app.Id, app.AuthScheme);
144-
}
158+
var result =
159+
scheme != null ?
160+
CreateScheme(name, scheme) :
161+
null;
145162

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

171+
private async Task<AppAuthScheme?> GetSchemeByAppAsync(string name,
172+
CancellationToken ct)
173+
{
174+
var app = await appStore.GetByAuthDomainAsync(name, ct);
175+
176+
return app?.AuthScheme;
177+
}
178+
179+
private async Task<AppAuthScheme?> GetSchemeByTempNameAsync(string name,
180+
CancellationToken ct)
181+
{
182+
var scheme = await temporarySchemes.GetAsync(name, ct);
183+
184+
return scheme;
185+
}
186+
154187
private SchemeResult CreateScheme(string name, AppAuthScheme config)
155188
{
156189
var scheme = new AuthenticationScheme(name, config.DisplayName, typeof(DynamicOpenIdConnectHandler));
157190

158191
var options = new DynamicOpenIdConnectOptions
159192
{
160-
Events = new OidcHandler(new OdicOptions
193+
Events = new OidcHandler(new OidcOptions
161194
{
162195
SignoutRedirectUrl = config.SignoutRedirectUrl
163196
}),
@@ -176,9 +209,8 @@ private SchemeResult CreateScheme(string name, AppAuthScheme config)
176209
return new SchemeResult(scheme, options);
177210
}
178211

179-
#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
180-
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
181-
private sealed record SchemeResult(AuthenticationScheme Scheme, DynamicOpenIdConnectOptions Options);
182-
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
183-
#pragma warning restore RECS0082 // Parameter has the same name as a member and hides it
212+
public IDisposable? OnChange(Action<DynamicOpenIdConnectOptions, string?> listener)
213+
{
214+
return null;
215+
}
184216
}

backend/src/Notifo.Domain/Apps/DeleteAppAuthScheme.cs backend/src/Notifo.Identity/Dynamic/IConfigurationStore.cs

+6-8
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
// All rights reserved. Licensed under the MIT license.
66
// ==========================================================================
77

8-
namespace Notifo.Domain.Apps;
8+
namespace Notifo.Identity.Dynamic;
99

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

17-
return new ValueTask<App?>(target);
18-
}
15+
Task<T?> GetAsync(string key,
16+
CancellationToken ct = default);
1917
}

backend/src/Notifo.Identity/IdentityServiceExtensions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Extensions.Configuration;
1414
using Microsoft.Extensions.Options;
1515
using Microsoft.IdentityModel.Logging;
16+
using Notifo.Domain.Apps;
1617
using Notifo.Domain.Identity;
1718
using Notifo.Identity;
1819
using Notifo.Identity.ApiKey;
@@ -153,6 +154,9 @@ public static void AddMyMongoDbIdentity(this IServiceCollection services)
153154
services.AddSingletonAs<MongoDbXmlRepository>()
154155
.As<IXmlRepository>();
155156

157+
services.AddSingletonAs<MongoDbConfigurationStore<AppAuthScheme>>()
158+
.As<IConfigurationStore<AppAuthScheme>>();
159+
156160
services.ConfigureOptions<MongoDbKeyOptions>();
157161

158162
services.Configure<KeyManagementOptions>((c, options) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// ==========================================================================
2+
// Notifo.io
3+
// ==========================================================================
4+
// Copyright (c) Sebastian Stehle
5+
// All rights reserved. Licensed under the MIT license.
6+
// ==========================================================================
7+
8+
namespace Notifo.Identity.MongoDb;
9+
10+
public sealed class MongoDbConfiguration<T>
11+
{
12+
public string Id { get; set; }
13+
14+
public T Value { get; set; }
15+
16+
public DateTime Expires { get; set; }
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// ==========================================================================
2+
// Notifo.io
3+
// ==========================================================================
4+
// Copyright (c) Sebastian Stehle
5+
// All rights reserved. Licensed under the MIT license.
6+
// ==========================================================================
7+
8+
using MongoDB.Driver;
9+
using Notifo.Identity.Dynamic;
10+
using Notifo.Infrastructure.MongoDb;
11+
12+
namespace Notifo.Identity.MongoDb;
13+
14+
public sealed class MongoDbConfigurationStore<T> : MongoDbRepository<MongoDbConfiguration<T>>, IConfigurationStore<T> where T : class
15+
{
16+
public MongoDbConfigurationStore(IMongoDatabase database)
17+
: base(database)
18+
{
19+
}
20+
21+
protected override string CollectionName()
22+
{
23+
return "Identity_Configuration";
24+
}
25+
26+
protected override Task SetupCollectionAsync(IMongoCollection<MongoDbConfiguration<T>> collection,
27+
CancellationToken ct = default)
28+
{
29+
return collection.Indexes.CreateOneAsync(
30+
new CreateIndexModel<MongoDbConfiguration<T>>(
31+
IndexKeys.Ascending(x => x.Expires),
32+
new CreateIndexOptions
33+
{
34+
ExpireAfter = TimeSpan.Zero
35+
}),
36+
cancellationToken: ct);
37+
}
38+
39+
public async Task<T?> GetAsync(string key,
40+
CancellationToken ct = default)
41+
{
42+
var entity = await Collection.Find(x => x.Id == key).FirstOrDefaultAsync(ct);
43+
44+
return entity?.Value;
45+
}
46+
47+
public async Task SetAsync(string key, T value, TimeSpan ttl,
48+
CancellationToken ct = default)
49+
{
50+
var expires = DateTime.UtcNow + ttl;
51+
52+
await Collection.UpdateOneAsync(
53+
Filter.Eq(x => x.Id, key),
54+
Update
55+
.SetOnInsert(x => x.Id, key)
56+
.Set(x => x.Value, value)
57+
.Set(x => x.Expires, expires),
58+
Upsert,
59+
cancellationToken: ct);
60+
}
61+
}

backend/src/Notifo.Identity/NotifoIdentityOptions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public sealed class NotifoIdentityOptions
1111
{
1212
public bool AllowPasswordAuth { get; set; }
1313

14+
public bool AllowCustomAuth { get; set; }
15+
1416
public string AdminClientId { get; set; }
1517

1618
public string AdminClientSecret { get; set; }

backend/src/Notifo.Identity/OidcHandler.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111

1212
namespace Notifo.Identity;
1313

14-
public class OdicOptions
14+
public class OidcOptions
1515
{
1616
public string? SignoutRedirectUrl { get; set; }
1717
}
1818

1919
public sealed class OidcHandler : OpenIdConnectEvents
2020
{
21-
private readonly OdicOptions options;
21+
private readonly OidcOptions options;
2222

23-
public OidcHandler(OdicOptions options)
23+
public OidcHandler(OidcOptions options)
2424
{
2525
this.options = options;
2626
}

backend/src/Notifo/Areas/Account/Controllers/AuthorizationController.cs

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public AuthorizationController(IOpenIddictScopeManager scopeManager, IOpenIddict
3232
}
3333

3434
[HttpPost("connect/token")]
35-
[Produces("application/json")]
3635
public async Task<IActionResult> Exchange()
3736
{
3837
var request = HttpContext.GetOpenIddictServerRequest();

backend/src/Notifo/Areas/Account/Controllers/UserInfoController.cs

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public class UserInfoController : ControllerBase<UserInfoController>
2020
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
2121
[HttpGet("connect/userinfo")]
2222
[HttpPost("connect/userinfo")]
23-
[Produces("application/json")]
2423
public async Task<IActionResult> Userinfo()
2524
{
2625
var user = await UserService.GetAsync(User, HttpContext.RequestAborted);

0 commit comments

Comments
 (0)