Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.0.1-preview</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
<PackageReference Include="MudBlazor" Version="9.0.0-preview.1" />
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="9.0.0-preview.2" />
<PackageReference Include="Scalar.AspNetCore" Version="2.12.18" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using CodeBeam.UltimateAuth.Server.Authentication;
using CodeBeam.UltimateAuth.Server.Defaults;
using CodeBeam.UltimateAuth.Server.Extensions;
using CodeBeam.UltimateAuth.Server.Options;
using CodeBeam.UltimateAuth.Sessions.InMemory;
using CodeBeam.UltimateAuth.Tokens.InMemory;
using CodeBeam.UltimateAuth.Users.InMemory;
Expand Down Expand Up @@ -54,7 +55,8 @@

builder.Services.AddUltimateAuth();

builder.Services.AddUltimateAuthServer(o => {
builder.Services.AddUltimateAuthServer(o =>
{
o.Diagnostics.EnableRefreshHeaders = true;
//o.Session.MaxLifetime = TimeSpan.FromSeconds(32);
//o.Session.TouchInterval = TimeSpan.FromSeconds(9);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
@using CodeBeam.UltimateAuth.Core.Options
@using Microsoft.Extensions.Options
@inject IJSRuntime JS
@inject IOptions<UAuthOptions> CoreOptions
@inject IOptions<UAuthClientOptions> Options
@inject NavigationManager Navigation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public async Task SubmitAsync()
await JS.InvokeVoidAsync("uauth.submitForm", _form);
}

private string ClientProfileValue => CoreOptions.Value.ClientProfile.ToString();
private string ClientProfileValue => Options.Value.ClientProfile.ToString();

private string EffectiveEndpoint => LoginType == UAuthLoginType.Pkce
? Options.Value.Endpoints.PkceComplete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol
// services.AddSingleton<IValidateOptions<UAuthClientOptions>, ...>();

services.AddSingleton<IClientProfileDetector, UAuthClientProfileDetector>();
services.AddSingleton<IPostConfigureOptions<UAuthOptions>, UAuthOptionsPostConfigure>();
services.AddSingleton<IPostConfigureOptions<UAuthClientOptions>, UAuthClientOptionsPostConfigure>();
services.TryAddSingleton<IClock, ClientClock>();

//services.PostConfigure<UAuthOptions>(o =>
Expand Down Expand Up @@ -107,9 +107,9 @@ private static IServiceCollection AddUltimateAuthClientInternal(this IServiceCol

services.AddScoped<ISessionCoordinator>(sp =>
{
var core = sp.GetRequiredService<IOptions<UAuthOptions>>().Value;
var options = sp.GetRequiredService<IOptions<UAuthClientOptions>>().Value;

return core.ClientProfile == UAuthClientProfile.BlazorServer
return options.ClientProfile == UAuthClientProfile.BlazorServer
? sp.GetRequiredService<BlazorServerSessionCoordinator>()
: sp.GetRequiredService<NoOpSessionCoordinator>();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CodeBeam.UltimateAuth.Client.Abstractions;
using CodeBeam.UltimateAuth.Client.Contracts;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Options;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
Expand All @@ -10,12 +11,12 @@ namespace CodeBeam.UltimateAuth.Client.Infrastructure;
internal sealed class UAuthRequestClient : IUAuthRequestClient
{
private readonly IJSRuntime _js;
private UAuthOptions _coreOptions;
private UAuthClientOptions _options;

public UAuthRequestClient(IJSRuntime js, IOptions<UAuthOptions> coreOptions)
public UAuthRequestClient(IJSRuntime js, IOptions<UAuthClientOptions> options)
{
_js = js;
_coreOptions = coreOptions.Value;
_options = options.Value;
}

public Task NavigateAsync(string endpoint, IDictionary<string, string>? form = null, CancellationToken ct = default)
Expand All @@ -27,7 +28,7 @@ public Task NavigateAsync(string endpoint, IDictionary<string, string>? form = n
url = endpoint,
mode = "navigate",
data = form,
clientProfile = _coreOptions.ClientProfile.ToString()
clientProfile = _options.ClientProfile.ToString()
}).AsTask();
}

Expand All @@ -41,7 +42,7 @@ public async Task<UAuthTransportResult> SendFormAsync(string endpoint, IDictiona
mode = "fetch",
expectJson = false,
data = form,
clientProfile = _coreOptions.ClientProfile.ToString()
clientProfile = _options.ClientProfile.ToString()
});

return result;
Expand All @@ -59,7 +60,7 @@ public async Task<UAuthTransportResult> SendFormForJsonAsync(string endpoint, ID
mode = "fetch",
expectJson = true,
data = postData,
clientProfile = _coreOptions.ClientProfile.ToString()
clientProfile = _options.ClientProfile.ToString()
});
}

Expand All @@ -71,7 +72,7 @@ public async Task<UAuthTransportResult> SendJsonAsync(string endpoint, object? p
{
url = endpoint,
payload = payload,
clientProfile = _coreOptions.ClientProfile.ToString()
clientProfile = _options.ClientProfile.ToString()
});
}

Expand Down
18 changes: 11 additions & 7 deletions src/CodeBeam.UltimateAuth.Client/Options/UAuthClientOptions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Options;

namespace CodeBeam.UltimateAuth.Client.Options;

public sealed class UAuthClientOptions
{
public UAuthClientProfile ClientProfile { get; set; } = UAuthClientProfile.NotSpecified;
public bool AutoDetectClientProfile { get; set; } = true;

public AuthEndpointOptions Endpoints { get; set; } = new();
public LoginOptions Login { get; set; } = new();
public LoginFlowOptions Login { get; set; } = new();

/// <summary>
/// Options related to PKCE-based login flows.
/// </summary>
public PkceLoginOptions Pkce { get; set; } = new();
public UAuthClientRefreshOptions Refresh { get; set; } = new();
public ReauthOptions Reauth { get; init; } = new();
}
Expand All @@ -27,19 +36,14 @@ public sealed class AuthEndpointOptions
public string HubLoginPath { get; set; } = "/uauthhub/login";
}

public sealed class LoginOptions
public sealed class LoginFlowOptions
{
/// <summary>
/// Default return URL after a successful login flow.
/// If not set, current location will be used.
/// </summary>
public string? DefaultReturnUrl { get; set; }

/// <summary>
/// Options related to PKCE-based login flows.
/// </summary>
public PkceLoginOptions Pkce { get; set; } = new();

/// <summary>
/// Enables or disables direct credential-based login.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
using CodeBeam.UltimateAuth.Core.Options;
using CodeBeam.UltimateAuth.Client.Options;
using CodeBeam.UltimateAuth.Core.Options;
using Microsoft.Extensions.Options;

namespace CodeBeam.UltimateAuth.Client.Infrastructure;

internal sealed class UAuthOptionsPostConfigure : IPostConfigureOptions<UAuthOptions>
internal sealed class UAuthClientOptionsPostConfigure : IPostConfigureOptions<UAuthClientOptions>
{
private readonly IClientProfileDetector _detector;
private readonly IServiceProvider _services;

public UAuthOptionsPostConfigure(IClientProfileDetector detector, IServiceProvider services)
public UAuthClientOptionsPostConfigure(IClientProfileDetector detector, IServiceProvider services)
{
_detector = detector;
_services = services;
}

public void PostConfigure(string? name, UAuthOptions options)
public void PostConfigure(string? name, UAuthClientOptions options)
{
if (!options.AutoDetectClientProfile)
return;
Expand Down
7 changes: 2 additions & 5 deletions src/CodeBeam.UltimateAuth.Client/Services/UAuthFlowClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,17 @@ internal class UAuthFlowClient : IFlowClient
{
private readonly IUAuthRequestClient _post;
private readonly UAuthClientOptions _options;
private readonly UAuthOptions _coreOptions;
private readonly UAuthClientDiagnostics _diagnostics;
private readonly NavigationManager _nav;

public UAuthFlowClient(
IUAuthRequestClient post,
IOptions<UAuthClientOptions> options,
IOptions<UAuthOptions> coreOptions,
UAuthClientDiagnostics diagnostics,
NavigationManager nav)
{
_post = post;
_options = options.Value;
_coreOptions = coreOptions.Value;
_diagnostics = diagnostics;
_nav = nav;
}
Expand Down Expand Up @@ -118,7 +115,7 @@ public async Task<AuthValidationResult> ValidateAsync()

public async Task BeginPkceAsync(string? returnUrl = null)
{
var pkce = _options.Login.Pkce;
var pkce = _options.Pkce;

if (!pkce.Enabled)
throw new InvalidOperationException("PKCE login is disabled by configuration.");
Expand Down Expand Up @@ -188,7 +185,7 @@ private Task NavigateToHubLoginAsync(string authorizationCode, string codeVerifi
["authorization_code"] = authorizationCode,
["code_verifier"] = codeVerifier,
["return_url"] = returnUrl,
["client_profile"] = _coreOptions.ClientProfile.ToString()
["client_profile"] = _options.ClientProfile.ToString()
};

return _post.NavigateAsync(hubLoginUrl, data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
/// </summary>
public interface IOpaqueTokenGenerator
{
string Generate(int byteLength = 32);
string Generate();
string GenerateJwtId();
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public interface ISessionIssuer

Task<IssuedSession> RotateSessionAsync(SessionRotationContext context, CancellationToken cancellationToken = default);

Task RevokeSessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset at, CancellationToken cancellationToken = default);
Task<bool> RevokeSessionAsync(TenantKey tenant, AuthSessionId sessionId, DateTimeOffset at, CancellationToken cancellationToken = default);

Task RevokeChainAsync(TenantKey tenant, SessionChainId chainId, DateTimeOffset at, CancellationToken cancellationToken = default);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;
public interface ISessionStoreKernel
{
Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default);
Task<TResult> ExecuteAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct = default);

Task<UAuthSession?> GetSessionAsync(AuthSessionId sessionId);
Task SaveSessionAsync(UAuthSession session);
Task RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at);
Task<bool> RevokeSessionAsync(AuthSessionId sessionId, DateTimeOffset at);

Task<UAuthSessionChain?> GetChainAsync(SessionChainId chainId);
Task SaveChainAsync(UAuthSessionChain chain);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,47 @@ public sealed class AccessContext

// Target
public string? Resource { get; init; }
public string? ResourceId { get; init; }
public UserKey? TargetUserKey { get; init; }
public TenantKey ResourceTenant { get; init; }

public string Action { get; init; } = default!;
public IReadOnlyDictionary<string, object> Attributes { get; init; } = EmptyAttributes.Instance;

public bool IsCrossTenant => !string.Equals(ActorTenant, ResourceTenant, StringComparison.Ordinal);
public bool IsSelfAction => ActorUserKey != null && ResourceId != null && string.Equals(ActorUserKey.Value, ResourceId, StringComparison.Ordinal);
public bool IsSelfAction => ActorUserKey != null && TargetUserKey != null && string.Equals(ActorUserKey.Value, TargetUserKey.Value, StringComparison.Ordinal);
public bool HasActor => ActorUserKey != null;
public bool HasTarget => ResourceId != null;
public bool HasTarget => TargetUserKey != null;

public UserKey GetTargetUserKey()
{
if (ResourceId is null)
throw new InvalidOperationException("Target user is not specified.");
if (TargetUserKey is not UserKey targetUserKey)
throw new InvalidOperationException("Target user is not found.");

return UserKey.Parse(ResourceId, null);
return targetUserKey;
}

internal AccessContext(
UserKey? actorUserKey,
TenantKey actorTenant,
bool isAuthenticated,
bool isSystemActor,
string resource,
UserKey? targetUserKey,
TenantKey resourceTenant,
string action,
IReadOnlyDictionary<string, object> attributes)
{
ActorUserKey = actorUserKey;
ActorTenant = actorTenant;
IsAuthenticated = isAuthenticated;
IsSystemActor = isSystemActor;

Resource = resource;
TargetUserKey = targetUserKey;
ResourceTenant = resourceTenant;

Action = action;
Attributes = attributes;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/Contracts/Logout/LogoutReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace CodeBeam.UltimateAuth.Core.Contracts;

public enum LogoutReason
{
Explicit,
SessionExpired,
SecurityPolicy,
AdminForced,
TenantDisabled
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class AuthenticatedSessionContext
public DateTimeOffset Now { get; init; }
public ClaimsSnapshot? Claims { get; init; }
public required SessionMetadata Metadata { get; init; }
public required UAuthMode Mode { get; init; }

/// <summary>
/// Optional chain identifier.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public sealed record SessionRotationContext
public required DeviceContext Device { get; init; }
public ClaimsSnapshot? Claims { get; init; }
public required SessionMetadata Metadata { get; init; } = SessionMetadata.Empty;
public required UAuthMode Mode { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public HubFlowArtifact(
string? returnUrl,
HubFlowPayload payload,
DateTimeOffset expiresAt)
: base(AuthArtifactType.HubFlow, expiresAt, maxAttempts: 1)
: base(AuthArtifactType.HubFlow, expiresAt)
{
HubSessionId = hubSessionId;
FlowType = flowType;
Expand Down
7 changes: 1 addition & 6 deletions src/CodeBeam.UltimateAuth.Core/Domain/Pkce/AuthArtifact.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,21 @@

public abstract class AuthArtifact
{
protected AuthArtifact(AuthArtifactType type, DateTimeOffset expiresAt, int maxAttempts)
protected AuthArtifact(AuthArtifactType type, DateTimeOffset expiresAt)
{
Type = type;
ExpiresAt = expiresAt;
MaxAttempts = maxAttempts;
}

public AuthArtifactType Type { get; }

public DateTimeOffset ExpiresAt { get; internal set; }

public int MaxAttempts { get; }

public int AttemptCount { get; private set; }
public bool IsCompleted { get; private set; }

public bool IsExpired(DateTimeOffset now) => now >= ExpiresAt;

public bool CanAttempt() => AttemptCount < MaxAttempts;

public void RegisterAttempt()
{
AttemptCount++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ public sealed class HubLoginArtifact : AuthArtifact
public HubLoginArtifact(
string authorizationCode,
string codeVerifier,
DateTimeOffset expiresAt,
int maxAttempts = 3)
: base(AuthArtifactType.HubLogin, expiresAt, maxAttempts)
DateTimeOffset expiresAt)
: base(AuthArtifactType.HubLogin, expiresAt)
{
AuthorizationCode = authorizationCode;
CodeVerifier = codeVerifier;
Expand Down
Loading