-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auth): support built-in OAuth methods #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
feb4a62
feat(auth): support built-in OAuth methods
yordis 074d2a9
fix(auth): honor configured authentication methods
yordis 6f30bb9
feat(auth): complete browser OAuth sign-in
yordis 881dfbf
fix(auth): harden OAuth browser sign-in
yordis b115fc6
fix(auth): preserve OAuth callback intent
yordis 17fa118
fix(auth): align OAuth browser flow with enabled methods
yordis 8d06615
fix(auth): avoid blocking signed-in OAuth recovery
yordis eaef68e
fix(auth): reject colliding authentication plugin names
yordis e5ef038
fix(auth): require scopes for OAuth browser flow
yordis 23c663e
fix(auth): preserve certificate authentication with OAuth
yordis 2354293
fix(auth): keep certificate users available with OAuth
yordis 4811929
fix(auth): hide password form without password method
yordis 80237c6
fix(auth): keep OAuth user management visible
yordis 1b09d8b
fix(auth): keep OAuth browser flow reachable
yordis 5231e52
fix(auth): preserve OAuth browser callback routing
yordis da592ae
fix(auth): avoid disabled UI OAuth redirects
yordis 5bfc10b
fix(auth): bind grouped authentication options
yordis 61e16e7
fix(auth): protect OAuth bearer routing
yordis dfed5b0
fix(auth): stop advertising legacy auth method
yordis ffbe9e8
fix(auth): validate OAuth callback tokens
yordis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
src/EventStore.Core.Tests/Authentication/CompositeAuthenticationProviderTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| using System.Collections.Generic; | ||
| using System.Security.Claims; | ||
| using System.Threading.Tasks; | ||
| using EventStore.Core.Authentication; | ||
| using EventStore.Plugins.Authentication; | ||
| using Microsoft.AspNetCore.Http; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace EventStore.Core.Tests.Authentication; | ||
|
|
||
| [TestFixture] | ||
| public class CompositeAuthenticationProviderTests | ||
| { | ||
| [Test] | ||
| public async Task routes_bearer_requests_to_bearer_provider() | ||
| { | ||
| var basicProvider = new RecordingAuthenticationProvider(["Basic"]); | ||
| var bearerProvider = new RecordingAuthenticationProvider(["Bearer"]); | ||
| var provider = new CompositeAuthenticationProvider([basicProvider, bearerProvider]); | ||
| var request = new HttpAuthenticationRequest(new DefaultHttpContext(), "jwt-token"); | ||
|
|
||
| provider.Authenticate(request); | ||
| await request.AuthenticateAsync(); | ||
|
|
||
| Assert.AreEqual(0, basicProvider.Calls); | ||
| Assert.AreEqual(1, bearerProvider.Calls); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task routes_password_requests_to_basic_provider() | ||
| { | ||
| var basicProvider = new RecordingAuthenticationProvider(["Basic"]); | ||
| var bearerProvider = new RecordingAuthenticationProvider(["Bearer"]); | ||
| var provider = new CompositeAuthenticationProvider([basicProvider, bearerProvider]); | ||
| var request = new HttpAuthenticationRequest(new DefaultHttpContext(), "admin", "changeit"); | ||
|
|
||
| provider.Authenticate(request); | ||
| await request.AuthenticateAsync(); | ||
|
|
||
| Assert.AreEqual(1, basicProvider.Calls); | ||
| Assert.AreEqual(0, bearerProvider.Calls); | ||
| } | ||
|
|
||
| private sealed class RecordingAuthenticationProvider(IReadOnlyList<string> schemes) : AuthenticationProviderBase | ||
| { | ||
| public int Calls { get; private set; } | ||
|
|
||
| public override void Authenticate(AuthenticationRequest authenticationRequest) | ||
| { | ||
| Calls++; | ||
| authenticationRequest.Authenticated( | ||
| new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Name, "test")], "test"))); | ||
| } | ||
|
|
||
| public override IReadOnlyList<string> GetSupportedAuthenticationSchemes() => schemes; | ||
| } | ||
| } |
101 changes: 101 additions & 0 deletions
101
src/EventStore.Core.Tests/Authentication/OAuthAuthenticationProviderTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| using System; | ||
| using System.Linq; | ||
| using System.Security.Claims; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using EventStore.Core.Authentication.OAuth; | ||
| using EventStore.Core.Services; | ||
| using EventStore.Plugins.Authentication; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.IdentityModel.JsonWebTokens; | ||
| using Microsoft.IdentityModel.Tokens; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace EventStore.Core.Tests.Authentication; | ||
|
|
||
| [TestFixture] | ||
| public class OAuthAuthenticationProviderTests | ||
| { | ||
| [Test] | ||
| public async Task authenticates_valid_bearer_token_and_maps_roles() | ||
| { | ||
| var signingKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray().Concat(Guid.NewGuid().ToByteArray()).ToArray()); | ||
| var token = CreateToken(signingKey, audience: "eventstore"); | ||
| var provider = new OAuthAuthenticationProvider( | ||
| new() | ||
| { | ||
| Issuer = "https://login.example.test", | ||
| Audiences = ["eventstore"], | ||
| NameClaimType = "sub", | ||
| RoleClaimType = "roles" | ||
| }, | ||
| logFailedAuthenticationAttempts: false, | ||
| _ => new ValueTask<TokenValidationParameters>(CreateValidationParameters(signingKey))); | ||
|
|
||
| var request = new HttpAuthenticationRequest(new DefaultHttpContext(), token); | ||
| provider.Authenticate(request); | ||
|
|
||
| var (status, principal) = await request.AuthenticateAsync(); | ||
|
|
||
| Assert.AreEqual(HttpAuthenticationRequestStatus.Authenticated, status); | ||
| Assert.AreEqual("alice", principal.Identity?.Name); | ||
| Assert.That(principal.HasClaim(ClaimTypes.Role, SystemRoles.Admins), Is.True); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task rejects_token_with_wrong_audience() | ||
| { | ||
| var signingKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray().Concat(Guid.NewGuid().ToByteArray()).ToArray()); | ||
| var token = CreateToken(signingKey, audience: "other-service"); | ||
| var provider = new OAuthAuthenticationProvider( | ||
| new() | ||
| { | ||
| Issuer = "https://login.example.test", | ||
| Audiences = ["eventstore"], | ||
| NameClaimType = "sub", | ||
| RoleClaimType = "roles" | ||
| }, | ||
| logFailedAuthenticationAttempts: false, | ||
| _ => new ValueTask<TokenValidationParameters>(CreateValidationParameters(signingKey))); | ||
|
|
||
| var request = new HttpAuthenticationRequest(new DefaultHttpContext(), token); | ||
| provider.Authenticate(request); | ||
|
|
||
| var (status, _) = await request.AuthenticateAsync(); | ||
|
|
||
| Assert.AreEqual(HttpAuthenticationRequestStatus.Unauthenticated, status); | ||
| } | ||
|
|
||
| private static string CreateToken(SecurityKey signingKey, string audience) | ||
| { | ||
| var descriptor = new SecurityTokenDescriptor | ||
| { | ||
| Issuer = "https://login.example.test", | ||
| Audience = audience, | ||
| Subject = new ClaimsIdentity([ | ||
| new Claim("sub", "alice"), | ||
| new Claim("roles", SystemRoles.Admins) | ||
| ]), | ||
| NotBefore = DateTime.UtcNow.AddMinutes(-1), | ||
| Expires = DateTime.UtcNow.AddMinutes(5), | ||
| SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256) | ||
| }; | ||
|
|
||
| return new JsonWebTokenHandler().CreateToken(descriptor); | ||
| } | ||
|
|
||
| private static TokenValidationParameters CreateValidationParameters(SecurityKey signingKey) => | ||
| new() | ||
| { | ||
| ValidateIssuer = true, | ||
| ValidIssuer = "https://login.example.test", | ||
| ValidateAudience = true, | ||
| ValidAudiences = ["eventstore"], | ||
| ValidateIssuerSigningKey = true, | ||
| IssuerSigningKey = signingKey, | ||
| ValidateLifetime = true, | ||
| ClockSkew = TimeSpan.Zero, | ||
| NameClaimType = "sub", | ||
| RoleClaimType = "roles" | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
src/EventStore.Core.XUnit.Tests/Authentication/AuthenticationMethodNamesTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| using EventStore.Core.Authentication; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace EventStore.Core.XUnit.Tests.Authentication; | ||
|
|
||
| public class AuthenticationMethodNamesTests | ||
| { | ||
| [Fact] | ||
| public void defaults_to_password_method() | ||
| { | ||
| AuthenticationMethodNames.FromOptions(new()).Should().Equal(AuthenticationMethodNames.Password); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void maps_legacy_internal_to_password() | ||
| { | ||
| AuthenticationMethodNames.FromOptions(new() { AuthenticationType = "internal" }) | ||
| .Should() | ||
| .Equal(AuthenticationMethodNames.Password); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void normalizes_multiple_methods() | ||
| { | ||
| AuthenticationMethodNames.FromOptions(new() { Methods = ["Password", "OAuth", "oauth"] }) | ||
| .Should() | ||
| .Equal(AuthenticationMethodNames.Password, AuthenticationMethodNames.OAuth); | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
src/EventStore.Core/Authentication/AuthenticationMethodNames.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
|
|
||
| namespace EventStore.Core.Authentication; | ||
|
|
||
| public static class AuthenticationMethodNames | ||
| { | ||
| public const string LegacyInternal = "internal"; | ||
|
yordis marked this conversation as resolved.
Outdated
|
||
| public const string Password = "password"; | ||
| public const string OAuth = "oauth"; | ||
|
|
||
| public static IReadOnlyList<string> FromOptions(ClusterVNodeOptions.AuthOptions options) | ||
| { | ||
| if (!IsLegacyInternal(options.AuthenticationType)) | ||
| { | ||
| return [Normalize(options.AuthenticationType)]; | ||
| } | ||
|
yordis marked this conversation as resolved.
Outdated
|
||
|
|
||
| var methods = options.Methods | ||
| .Where(method => !string.IsNullOrWhiteSpace(method)) | ||
| .Select(Normalize) | ||
| .Distinct(StringComparer.OrdinalIgnoreCase) | ||
| .ToArray(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return methods.Length == 0 ? [Password] : methods; | ||
| } | ||
|
|
||
| public static bool IncludesPassword(ClusterVNodeOptions.AuthOptions options) => | ||
| FromOptions(options).Any(IsPassword); | ||
|
|
||
| public static string Normalize(string method) => | ||
| IsLegacyInternal(method) ? Password : method.Trim().ToLowerInvariant(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| public static bool IsPassword(string method) => | ||
| string.Equals(Normalize(method), Password, StringComparison.OrdinalIgnoreCase); | ||
|
|
||
| private static bool IsLegacyInternal(string method) => | ||
| string.Equals(method?.Trim(), LegacyInternal, StringComparison.OrdinalIgnoreCase); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.