Skip to content
Merged
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
5 changes: 4 additions & 1 deletion UltimateAuth.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
<File Path="Roadmap.md" />
</Folder>
<Folder Name="/Tests/">
<Project Path="tests/CodeBeam.UltimateAuth.Core.Tests/CodeBeam.UltimateAuth.Core.Tests.csproj" Id="6f4b22da-849a-4a79-b5c5-aee7cb1429a6" />
<Project Path="tests/CodeBeam.UltimateAuth.Tests.Unit/CodeBeam.UltimateAuth.Tests.Unit.csproj" Id="6f4b22da-849a-4a79-b5c5-aee7cb1429a6" />
</Folder>
<Project Path="src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj" Id="eb60a3b7-ba9d-48c9-98ad-b28e879b23bf" />
<Project Path="src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj" />
<Project Path="src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj" Id="0a8cdd12-a8c4-4530-87e8-ae778c46322b" />
<Project Path="src/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Server.Users.csproj" Id="30d5db36-6dc8-46f6-9139-8b6b3d6053d5" />
<Project Path="src/credentials/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore/CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.csproj" Id="1fd362d5-864b-4bb3-97be-9095d94cfdba" />
<Project Path="src/credentials/CodeBeam.UltimateAuth.Credentials.InMemory/CodeBeam.UltimateAuth.Credentials.InMemory.csproj" Id="62ee7b1d-46ce-4f2e-985d-1e794f891b8b" />
<Project Path="src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj" Id="6abfb7a6-ea36-42db-a843-38054dd40fd8" />
<Project Path="src/sessions/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore/CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.csproj" Id="5b9a090d-1689-4a81-9dfa-3ba69f0bda38" />
<Project Path="src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj" Id="fc9bfef0-8a89-4639-81ee-3f84f6e33816" />
<Project Path="src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.csproj" Id="6eb14b32-0b56-460f-a2b2-f95d28bad625" />
<Project Path="src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj" Id="8220884e-4958-4b49-8c69-56ce9d2b6c6f" />
</Solution>
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,31 @@ public interface ISessionStore<TUserId>
/// <summary>
/// Retrieves an active session by id.
/// </summary>
Task<ISession<TUserId>?> GetSessionAsync(
string? tenantId,
AuthSessionId sessionId);
Task<ISession<TUserId>?> GetSessionAsync(string? tenantId, AuthSessionId sessionId, CancellationToken ct = default);

/// <summary>
/// Creates a new session and associates it with the appropriate chain and root.
/// </summary>
Task CreateSessionAsync(
IssuedSession<TUserId> issuedSession,
SessionStoreContext<TUserId> context);
Task CreateSessionAsync(IssuedSession<TUserId> issuedSession, SessionStoreContext<TUserId> context, CancellationToken ct = default);

/// <summary>
/// Refreshes (rotates) the active session within its chain.
/// </summary>
Task RotateSessionAsync(
AuthSessionId currentSessionId,
IssuedSession<TUserId> newSession,
SessionStoreContext<TUserId> context);
Task RotateSessionAsync(AuthSessionId currentSessionId, IssuedSession<TUserId> newSession, SessionStoreContext<TUserId> context, CancellationToken ct = default);

/// <summary>
/// Revokes a single session.
/// </summary>
Task RevokeSessionAsync(
string? tenantId,
AuthSessionId sessionId,
DateTimeOffset at);
Task RevokeSessionAsync(string? tenantId, AuthSessionId sessionId, DateTimeOffset at, CancellationToken ct = default);

/// <summary>
/// Revokes all sessions for a specific user (all devices).
/// </summary>
Task RevokeAllSessionsAsync(
string? tenantId,
TUserId userId,
DateTimeOffset at);
Task RevokeAllSessionsAsync(string? tenantId, TUserId userId, DateTimeOffset at, CancellationToken ct = default);

/// <summary>
/// Revokes all sessions within a specific chain (single device).
/// </summary>
Task RevokeChainAsync(
string? tenantId,
ChainId chainId,
DateTimeOffset at);
Task RevokeChainAsync(string? tenantId, ChainId chainId, DateTimeOffset at, CancellationToken ct = default);
}
}
3 changes: 3 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore")]
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
namespace CodeBeam.UltimateAuth.Core.Contracts
{
/// <summary>
/// Represents an issued refresh token.
/// Always opaque and hashed at rest.
/// Transport model for refresh token. Returned to client once upon creation.
/// </summary>
public sealed class RefreshToken
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public static bool TryCreate(string raw, out AuthSessionId sessionId)
/// </summary>
public string Value { get; }

public static AuthSessionId From(string value) => new(value);

/// <summary>
/// Determines whether the specified <see cref="AuthSessionId"/> is equal to the current instance.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/Domain/Session/ChainId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public ChainId(Guid value)
/// <returns>A new <see cref="ChainId"/> instance.</returns>
public static ChainId New() => new ChainId(Guid.NewGuid());

public static ChainId From(Guid value) => new(value);

/// <summary>
/// Determines whether the specified <see cref="ChainId"/> is equal to the current instance.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/Domain/Session/DeviceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
/// </summary>
public sealed class DeviceInfo
{
// TODO: Implement DeviceId and makes it first-class citizen in security policies.
/// <summary>
/// Gets the unique identifier for the device.
/// No session should be created without a device id.
/// </summary>
public string DeviceId { get; init; } = default!;

Expand Down Expand Up @@ -72,6 +74,17 @@ public sealed class DeviceInfo
IsTrusted = null
};

// TODO: Empty may not be good approach, make strict security here
public static DeviceInfo Empty { get; } = new()
{
DeviceId = "",
Platform = null,
Browser = null,
IpAddress = null,
UserAgent = null,
IsTrusted = null
};

/// <summary>
/// Determines whether the current device information matches the specified device information based on device
/// identifiers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface ISessionChain<TUserId>
/// </summary>
ChainId ChainId { get; }

string? TenantId { get; }

/// <summary>
/// Gets the identifier of the user who owns this chain.
/// Each chain represents one device/login family for this user.
Expand Down
32 changes: 32 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@ public ISession<TUserId> Revoke(DateTimeOffset at)
);
}

internal static UAuthSession<TUserId> FromProjection(
AuthSessionId sessionId,
string? tenantId,
TUserId userId,
ChainId chainId,
DateTimeOffset createdAt,
DateTimeOffset expiresAt,
DateTimeOffset? lastSeenAt,
bool isRevoked,
DateTimeOffset? revokedAt,
long securityVersionAtCreation,
DeviceInfo device,
ClaimsSnapshot claims,
SessionMetadata metadata)
{
return new UAuthSession<TUserId>(
sessionId,
tenantId,
userId,
chainId,
createdAt,
expiresAt,
lastSeenAt,
isRevoked,
revokedAt,
securityVersionAtCreation,
device,
claims,
metadata
);
}

public SessionState GetState(DateTimeOffset at)
{
if (IsRevoked) return SessionState.Revoked;
Expand Down
24 changes: 24 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,29 @@ public ISessionChain<TUserId> Revoke(DateTimeOffset at)
);
}

internal static UAuthSessionChain<TUserId> FromProjection(
ChainId chainId,
string? tenantId,
TUserId userId,
int rotationCount,
long securityVersionAtCreation,
ClaimsSnapshot claimsSnapshot,
AuthSessionId? activeSessionId,
bool isRevoked,
DateTimeOffset? revokedAt)
{
return new UAuthSessionChain<TUserId>(
chainId,
tenantId,
userId,
rotationCount,
securityVersionAtCreation,
claimsSnapshot,
activeSessionId,
isRevoked,
revokedAt
);
}

}
}
21 changes: 21 additions & 0 deletions src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSessionRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,26 @@ public ISessionRoot<TUserId> AttachChain(ISessionChain<TUserId> chain, DateTimeO
);
}

internal static UAuthSessionRoot<TUserId> FromProjection(
string? tenantId,
TUserId userId,
bool isRevoked,
DateTimeOffset? revokedAt,
long securityVersion,
IReadOnlyList<ISessionChain<TUserId>> chains,
DateTimeOffset lastUpdatedAt)
{
return new UAuthSessionRoot<TUserId>(
tenantId,
userId,
isRevoked,
revokedAt,
securityVersion,
chains,
lastUpdatedAt
);
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ public enum SessionRefreshStatus
Success,
ReauthRequired,
InvalidRequest,
Failed = 3
Failed
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore;

internal sealed class CredentialUserMapping<TUser, TUserId>
{
public Func<TUser, TUserId> UserId { get; init; } = default!;
public Func<TUser, string> Username { get; init; } = default!;
public Func<TUser, string> PasswordHash { get; init; } = default!;
public Func<TUser, long> SecurityVersion { get; init; } = default!;
public Func<TUser, bool> CanAuthenticate { get; init; } = default!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.1-preview</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.22" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\CodeBeam.UltimateAuth.Core\CodeBeam.UltimateAuth.Core.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Linq.Expressions;

namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore
{
internal static class ConventionResolver
{
public static Expression<Func<TUser, TProp>>? TryResolve<TUser, TProp>(params string[] names)
{
var prop = typeof(TUser)
.GetProperties()
.FirstOrDefault(p =>
names.Contains(p.Name, StringComparer.OrdinalIgnoreCase) &&
typeof(TProp).IsAssignableFrom(p.PropertyType));

if (prop is null)
return null;

var param = Expression.Parameter(typeof(TUser), "u");
var body = Expression.Property(param, prop);

return Expression.Lambda<Func<TUser, TProp>>(body, param);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore
{
internal static class CredentialUserMappingBuilder
{
public static CredentialUserMapping<TUser, TUserId> Build<TUser, TUserId>(CredentialUserMappingOptions<TUser, TUserId> options)
{
if (options.UserId is null)
{
var expr = ConventionResolver.TryResolve<TUser, TUserId>("Id", "UserId");
if (expr != null)
options.ApplyUserId(expr);
}

if (options.Username is null)
{
var expr = ConventionResolver.TryResolve<TUser, string>(
"Username",
"UserName",
"Email",
"EmailAddress",
"Login");

if (expr != null)
options.ApplyUsername(expr);
}

// Never add "Password" as a convention to avoid accidental mapping to plaintext password properties
if (options.PasswordHash is null)
{
var expr = ConventionResolver.TryResolve<TUser, string>(
"PasswordHash",
"Passwordhash",
"PasswordHashV2");

if (expr != null)
options.ApplyPasswordHash(expr);
}

if (options.SecurityVersion is null)
{
var expr = ConventionResolver.TryResolve<TUser, long>(
"SecurityVersion",
"SecurityStamp",
"AuthVersion");

if (expr != null)
options.ApplySecurityVersion(expr);
}


if (options.UserId is null)
throw new InvalidOperationException("UserId mapping is required. Use MapUserId(...) or ensure a conventional property exists.");

if (options.Username is null)
throw new InvalidOperationException("Username mapping is required. Use MapUsername(...) or ensure a conventional property exists.");

if (options.PasswordHash is null)
throw new InvalidOperationException("PasswordHash mapping is required. Use MapPasswordHash(...) or ensure a conventional property exists.");

if (options.SecurityVersion is null)
throw new InvalidOperationException("SecurityVersion mapping is required. Use MapSecurityVersion(...) or ensure a conventional property exists.");

var canAuthenticateExpr = options.CanAuthenticate ?? (_ => true);

return new CredentialUserMapping<TUser, TUserId>
{
UserId = options.UserId.Compile(),
Username = options.Username.Compile(),
PasswordHash = options.PasswordHash.Compile(),
SecurityVersion = options.SecurityVersion.Compile(),
CanAuthenticate = canAuthenticateExpr.Compile()
};
}
}
}
Loading
Loading