Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
feb4a62
feat(auth): support built-in OAuth methods
yordis Jul 15, 2026
074d2a9
fix(auth): honor configured authentication methods
yordis Jul 15, 2026
6f30bb9
feat(auth): complete browser OAuth sign-in
yordis Jul 15, 2026
881dfbf
fix(auth): harden OAuth browser sign-in
yordis Jul 15, 2026
b115fc6
fix(auth): preserve OAuth callback intent
yordis Jul 15, 2026
17fa118
fix(auth): align OAuth browser flow with enabled methods
yordis Jul 15, 2026
8d06615
fix(auth): avoid blocking signed-in OAuth recovery
yordis Jul 15, 2026
eaef68e
fix(auth): reject colliding authentication plugin names
yordis Jul 15, 2026
e5ef038
fix(auth): require scopes for OAuth browser flow
yordis Jul 15, 2026
23c663e
fix(auth): preserve certificate authentication with OAuth
yordis Jul 15, 2026
2354293
fix(auth): keep certificate users available with OAuth
yordis Jul 15, 2026
4811929
fix(auth): hide password form without password method
yordis Jul 15, 2026
80237c6
fix(auth): keep OAuth user management visible
yordis Jul 15, 2026
1b09d8b
fix(auth): keep OAuth browser flow reachable
yordis Jul 15, 2026
5231e52
fix(auth): preserve OAuth browser callback routing
yordis Jul 15, 2026
da592ae
fix(auth): avoid disabled UI OAuth redirects
yordis Jul 15, 2026
5bfc10b
fix(auth): bind grouped authentication options
yordis Jul 15, 2026
61e16e7
fix(auth): protect OAuth bearer routing
yordis Jul 15, 2026
dfed5b0
fix(auth): stop advertising legacy auth method
yordis Jul 15, 2026
ffbe9e8
fix(auth): validate OAuth callback tokens
yordis Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.9" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.2" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.19.2" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
Expand Down
39 changes: 28 additions & 11 deletions src/EventStore.ClusterNode/ClusterVNodeHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using EventStore.Core;
using EventStore.Core.Authentication;
using EventStore.Core.Authentication.InternalAuthentication;
using EventStore.Core.Authentication.OAuth;
using EventStore.Core.Authentication.PassthroughAuthentication;
using EventStore.Core.Authorization;
using EventStore.Core.Certificates;
Expand Down Expand Up @@ -246,10 +247,18 @@ AuthenticationProviderFactory GetAuthenticationProviderFactory()
return new AuthenticationProviderFactory(_ => new PassthroughAuthenticationProviderFactory());
}

var authenticationTypeToPlugin = new Dictionary<string, AuthenticationProviderFactory> {
var authenticationMethodFactories = new Dictionary<string, AuthenticationProviderFactory> {
{
"internal", new AuthenticationProviderFactory(components =>
AuthenticationMethodNames.LegacyInternal, new AuthenticationProviderFactory(components =>
new InternalAuthenticationProviderFactory(components, _options.DefaultUser))
},
{
AuthenticationMethodNames.Password, new AuthenticationProviderFactory(components =>
new InternalAuthenticationProviderFactory(components, _options.DefaultUser))
},
{
AuthenticationMethodNames.OAuth, new AuthenticationProviderFactory(_ =>
new OAuthAuthenticationProviderFactory(_options.Auth.OAuth))
}
};

Expand All @@ -261,7 +270,7 @@ AuthenticationProviderFactory GetAuthenticationProviderFactory()
Log.Information(
"Loaded authentication plugin: {plugin} version {version} (Command Line: {commandLine})",
potentialPlugin.Name, potentialPlugin.Version, commandLine);
authenticationTypeToPlugin.Add(commandLine,
authenticationMethodFactories.Add(commandLine,
new AuthenticationProviderFactory(_ =>
potentialPlugin.GetAuthenticationProviderFactory(authenticationConfig)));
}
Expand All @@ -271,14 +280,22 @@ AuthenticationProviderFactory GetAuthenticationProviderFactory()
}
}

return authenticationTypeToPlugin.TryGetValue(_options.Auth.AuthenticationType.ToLowerInvariant(),
out var factory)
Comment thread
yordis marked this conversation as resolved.
? factory
: throw new ApplicationInitializationException(
$"The authentication type {_options.Auth.AuthenticationType} is not recognised. If this is supposed " +
$"to be provided by an authentication plugin, confirm the plugin DLL is located in {Locations.PluginsDirectory}." +
Environment.NewLine +
$"Valid options for authentication are: {string.Join(", ", authenticationTypeToPlugin.Keys)}.");
var methods = AuthenticationMethodNames.FromOptions(_options.Auth);
foreach (var method in methods)
{
if (!authenticationMethodFactories.ContainsKey(method))
{
throw new ApplicationInitializationException(
$"The authentication method {method} is not recognised. If this is supposed " +
$"to be provided by an authentication plugin, confirm the plugin DLL is located in {Locations.PluginsDirectory}." +
Environment.NewLine +
$"Valid options for authentication are: {string.Join(", ", authenticationMethodFactories.Keys)}.");
}
}

return new AuthenticationProviderFactory(components =>
new CompositeAuthenticationProviderFactory(methods.Select(method =>
authenticationMethodFactories[method].GetFactory(components)).ToArray()));
}

static ClusterVNodeOptions LoadSubsystemsPlugins(PluginLoader pluginLoader, ClusterVNodeOptions options)
Expand Down
38 changes: 32 additions & 6 deletions src/EventStore.ClusterNode/Components/Pages/SignIn.razor
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,34 @@
<p class="mt-3 text-sm leading-6 text-es-muted">This node is running with insecure access, so there is no credential exchange to perform.</p>
<a class="mt-6 inline-flex rounded-2xl bg-es-ink px-5 py-3 text-sm font-black text-white shadow-lg shadow-es-ink/15 transition hover:bg-es-green" href="@Destination">Continue to UI</a>
}
else if (!Authentication.SupportsBasic)
else if (!Authentication.SupportsBasic && !Authentication.SupportsOAuthBrowserFlow)
{
<StatusBadge Label="@Authentication.TypeLabel" Tone="warn" />
<h2 class="mt-4 text-3xl font-black tracking-tight text-es-ink">Continue with the configured provider.</h2>
<p class="mt-3 text-sm leading-6 text-es-muted">This node does not advertise Basic authentication, so sign-in is delegated to the configured browser authentication flow.</p>
<button class="mt-6 rounded-2xl bg-es-ink px-5 py-3 text-sm font-black text-white shadow-lg shadow-es-ink/15 transition hover:bg-es-green" type="button" data-ui-oauth-signin data-ui-oauth-return="@Destination" data-ui-oauth-type="@Authentication.Type" data-ui-oauth-properties="@Authentication.PropertiesJson">Continue</button>
<p class="mt-4 hidden rounded-2xl border border-red-200 bg-red-50 p-4 text-sm font-bold text-red-800" data-ui-oauth-status></p>
<h2 class="mt-4 text-3xl font-black tracking-tight text-es-ink">Sign-in needs more configuration.</h2>
<p class="mt-3 text-sm leading-6 text-es-muted">This node does not advertise Basic authentication or a browser OAuth flow.</p>
@if (!string.IsNullOrWhiteSpace(Message))
{
<p class="mt-4 rounded-2xl border border-red-200 bg-red-50 p-4 text-sm font-bold text-red-800">@Message</p>
}
}
else
{
@if (Authentication.SupportsOAuthBrowserFlow)
{
<div class="mb-6 rounded-[1.5rem] border border-es-ink/10 bg-es-cloud/70 p-5">
<h2 class="text-2xl font-black tracking-tight text-es-ink">Continue with OAuth</h2>
<p class="mt-2 text-sm leading-6 text-es-muted">Use the configured browser sign-in flow for this node.</p>
<button class="mt-4 rounded-2xl bg-es-ink px-5 py-3 text-sm font-black text-white shadow-lg shadow-es-ink/15 transition hover:bg-es-green" type="button" data-ui-oauth-signin data-ui-oauth-return="@Destination" data-ui-oauth-type="@Authentication.Type" data-ui-oauth-properties="@Authentication.PropertiesJson">Continue with OAuth</button>
@if (!string.IsNullOrWhiteSpace(Message))
{
<p class="mt-4 rounded-2xl border border-red-200 bg-red-50 p-4 text-sm font-bold text-red-800">@Message</p>
}
<p class="mt-4 hidden rounded-2xl border border-red-200 bg-red-50 p-4 text-sm font-bold text-red-800" data-ui-oauth-status></p>
</div>
}

@if (Authentication.SupportsBasic)
{
<EditForm Model="Input" FormName="ui-signin" OnValidSubmit="SignInUser">
<AntiforgeryToken />
<DataAnnotationsValidator />
Expand Down Expand Up @@ -74,6 +92,7 @@
<a class="rounded-2xl border border-es-ink/10 bg-white px-5 py-3 text-sm font-black text-es-ink transition hover:border-es-green/30 hover:text-es-forest" href="/ui">Back to UI</a>
</div>
</EditForm>
}
}
</article>
</section>
Expand All @@ -82,6 +101,9 @@
[SupplyParameterFromQuery(Name = "returnUrl")]
private string ReturnUrl { get; set; } = "";

[SupplyParameterFromQuery(Name = "oauth_error")]
private string OAuthError { get; set; } = "";

[SupplyParameterFromForm(FormName = "ui-signin")]
private SignInInput Input { get; set; }

Expand All @@ -96,6 +118,9 @@
try {
Authentication = Security.AuthenticationInfo();
AuthenticationAvailable = true;
Message = string.IsNullOrWhiteSpace(OAuthError)
? ""
: "The OAuth sign-in flow could not be completed.";
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
yordis marked this conversation as resolved.
Outdated
} catch (Exception ex) {
Authentication = SecurityAuthenticationInfo.Unavailable();
AuthenticationAvailable = false;
Expand All @@ -112,7 +137,8 @@
private static bool IsOAuthCallback(HttpRequest request) =>
request.Query.ContainsKey("code") ||
request.Query.ContainsKey("state") ||
request.Query.ContainsKey("error");
request.Query.ContainsKey("error") ||
request.Query.ContainsKey("oauth_error");
Comment thread
yordis marked this conversation as resolved.
Outdated

private async Task SignInUser() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using EventStore.Common.Options;
using EventStore.Common.Utils;
using EventStore.Core;
using EventStore.Core.Authentication;
using EventStore.Core.Data;
using EventStore.Core.Services;
using EventStore.Core.Util;
Expand All @@ -22,7 +23,7 @@ public ConfigurationPage Read()
var features = new[] {
new ConfigurationFeature("Projections", options.Projection.RunProjections != ProjectionType.None || options.DevMode.Dev),
new ConfigurationFeature("User management",
options.Auth.AuthenticationType == Opts.AuthenticationTypeDefault && !options.Application.AuthDisabled())
AuthenticationMethodNames.IncludesPassword(options.Auth) && !options.Application.AuthDisabled())
};

var subsystems = hostedService.EnabledNodeSubsystems
Expand All @@ -44,7 +45,7 @@ public ConfigurationPage Read()
canInspectRuntimeDetails ? new IPEndPoint(options.Interface.NodeIp, options.Interface.NodePort).ToString() : "",
canInspectRuntimeDetails ? options.Database.Db : "",
canInspectRuntimeDetails ? options.Database.DbLogFormat.ToString() : "",
canInspectRuntimeDetails ? options.Auth.AuthenticationType : "",
canInspectRuntimeDetails ? string.Join(", ", AuthenticationMethodNames.FromOptions(options.Auth)) : "",
canInspectRuntimeDetails ? options.Auth.AuthorizationType : "",
options.Application.Insecure,
options.Application.TlsDisabled(),
Expand Down
Loading
Loading