![]() |
VOOZH | about |
dotnet add package SiLA2.Authentication --version 10.2.4
NuGet\Install-Package SiLA2.Authentication -Version 10.2.4
<PackageReference Include="SiLA2.Authentication" Version="10.2.4" />
<PackageVersion Include="SiLA2.Authentication" Version="10.2.4" />Directory.Packages.props
<PackageReference Include="SiLA2.Authentication" />Project file
paket add SiLA2.Authentication --version 10.2.4
#r "nuget: SiLA2.Authentication, 10.2.4"
#:package SiLA2.Authentication@10.2.4
#addin nuget:?package=SiLA2.Authentication&version=10.2.4Install as a Cake Addin
#tool nuget:?package=SiLA2.Authentication&version=10.2.4Install as a Cake Tool
| NuGet Package | SiLA2.Authentication on NuGet.org |
| Repository | https://gitlab.com/SiLA2/sila_csharp |
| SiLA Standard | https://sila-standard.com |
| License | MIT |
SiLA2.Authentication is an optional module for the SiLA2 .NET implementation that provides secure user authentication and role-based authorization for laboratory automation servers. This module enables SiLA2 servers to authenticate users, manage user accounts, and control access to features based on user roles.
The library is designed with a database-agnostic architecture, providing interface abstractions that can be implemented for any storage technology (SQL Server, PostgreSQL, MongoDB, etc.) while including a production-ready SQLite implementation out of the box.
SiLA2.Authentication is an optional module that extends the base SiLA2 server functionality. While SiLA2.Core provides the fundamental server infrastructure, this module adds authentication and authorization capabilities for secure laboratory workflows.
When to use this module:
Install via NuGet Package Manager:
dotnet add package SiLA2.Authentication
Or via Package Manager Console:
Install-Package SiLA2.Authentication
Get authentication working in your SiLA2 server in 3 steps:
dotnet add package SiLA2.Authentication
using SiLA2.Authentication.Extensions;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Register SiLA2 Authentication with default SQLite implementation
builder.Services.AddSiLA2Authentication(options =>
{
options.UseSqlite("Data Source=users.db");
});
// Register other SiLA2 services...
builder.Services.AddSingleton<ISiLA2Server, SiLA2Server>();
builder.Services.AddGrpc();
var app = builder.Build();
// Initialize authentication database (creates tables and seeds default users)
app.Services.EnsureAuthenticationDatabaseCreated();
// Map gRPC services and start server...
app.MapGrpcService<MySiLA2Service>();
app.Run();
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
public class MyFeatureService : MyFeature.MyFeatureBase
{
private readonly IAuthenticationService _authService;
private readonly IUserManager _userManager;
public MyFeatureService(
IAuthenticationService authService,
IUserManager userManager)
{
_authService = authService;
_userManager = userManager;
}
public override async Task<Response> SecureCommand(
Parameters request,
ServerCallContext context)
{
// Authenticate user from request metadata
var username = context.GetHttpContext()?.User?.Identity?.Name ?? "anonymous";
var password = ExtractPasswordFromMetadata(context.RequestHeaders);
var result = await _authService.AuthenticateAsync(username, password);
if (!result.IsAuthenticated)
{
throw new RpcException(new Status(
StatusCode.Unauthenticated,
"Invalid credentials"));
}
// Check user role
if (result.User.Role != Role.Admin)
{
throw new RpcException(new Status(
StatusCode.PermissionDenied,
"Admin access required"));
}
// Execute command logic...
return new Response();
}
}
That's it! You now have a fully functional authentication system with:
Security Warning: Default credentials are for development only. Change them immediately in production!
The module provides a clean separation between interface contracts and implementations:
Core authentication operations for login and password management.
public interface IAuthenticationService
{
// Basic authentication
Task<AuthenticationResult> AuthenticateAsync(
string login,
string password,
CancellationToken cancellationToken = default);
// Server-specific authentication
Task<AuthenticationResult> AuthenticateForServerAsync(
string login,
string password,
Guid requestedServerId,
Guid serverConfigId,
CancellationToken cancellationToken = default);
// Password operations
bool ValidatePassword(string passwordHash, string password);
string HashPassword(string password);
Task<bool> ChangePasswordAsync(
Guid userId,
string currentPassword,
string newPassword,
CancellationToken cancellationToken = default);
}
High-level user management combining repository and authentication functionality.
public interface IUserManager
{
// User CRUD operations
Task<User> CreateUserAsync(string login, string password, Role role = Role.Standard, CancellationToken cancellationToken = default);
Task<User> GetUserByIdAsync(Guid userId, CancellationToken cancellationToken = default);
Task<User> GetUserByLoginAsync(string login, CancellationToken cancellationToken = default);
Task<IEnumerable<User>> GetAllUsersAsync(CancellationToken cancellationToken = default);
Task<User> UpdateUserAsync(User user, CancellationToken cancellationToken = default);
Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken = default);
// Role management
Task<User> UpdateUserRoleAsync(Guid userId, Role newRole, CancellationToken cancellationToken = default);
// Password management
Task<bool> UpdateUserPasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken = default);
// Queries
Task<bool> UserExistsAsync(string login, CancellationToken cancellationToken = default);
IQueryable<User> GetUsers();
}
Database-agnostic interface for user data access. Implement this interface to use custom storage backends.
public interface IUserRepository
{
Task<User> GetByIdAsync(Guid userId, CancellationToken cancellationToken = default);
Task<User> GetByLoginAsync(string login, CancellationToken cancellationToken = default);
Task<IEnumerable<User>> GetAllAsync(CancellationToken cancellationToken = default);
IQueryable<User> GetQueryable();
Task<User> CreateAsync(User user, CancellationToken cancellationToken = default);
Task<User> UpdateAsync(User user, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid userId, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(string login, CancellationToken cancellationToken = default);
}
Validates authentication requests with server identity checking.
public interface IAuthenticationInspector
{
Task<bool> IsAuthenticatedAsync(
string userIdentification,
string password,
Guid requestedServerId,
CancellationToken cancellationToken = default);
}
Represents a user account with credentials and metadata.
public class User
{
public Guid Id { get; set; }
public string Login { get; set; }
public string PasswordHash { get; set; } // Never plain text!
public Role Role { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
User roles for authorization.
[Flags]
public enum Role
{
Admin = 0, // Full access
Standard = 1, // Limited access
Undefined = 2 // No defined role
}
Result of an authentication attempt.
public class AuthenticationResult
{
public bool IsAuthenticated { get; set; }
public User User { get; set; }
public string ErrorMessage { get; set; }
public AuthenticationFailureReason FailureReason { get; set; }
public static AuthenticationResult Success(User user);
public static AuthenticationResult Failure(AuthenticationFailureReason reason, string message);
}
public enum AuthenticationFailureReason
{
None,
UserNotFound,
InvalidPassword,
InvalidServer,
InternalError
}
The module includes a production-ready SQLite implementation:
The simplest setup uses the default SQLite implementation:
using SiLA2.Authentication.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Register authentication with default SQLite database
builder.Services.AddSiLA2Authentication(options =>
{
options.UseSqlite("Data Source=users.db");
});
var app = builder.Build();
// Initialize database (creates tables, runs migrations, seeds default users)
app.Services.EnsureAuthenticationDatabaseCreated();
app.Run();
For enterprise deployments with SQL Server:
using SiLA2.Authentication.Extensions;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Register authentication with SQL Server
builder.Services.AddSiLA2Authentication(options =>
{
options.UseSqlServer(
builder.Configuration.GetConnectionString("AuthenticationDatabase"),
sqlOptions => sqlOptions.EnableRetryOnFailure()
);
});
var app = builder.Build();
app.Services.EnsureAuthenticationDatabaseCreated();
app.Run();
Connection String (appsettings.json):
{
"ConnectionStrings": {
"AuthenticationDatabase": "Server=localhost;Database=SiLA2Auth;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
For PostgreSQL deployments:
builder.Services.AddSiLA2Authentication(options =>
{
options.UseNpgsql(
builder.Configuration.GetConnectionString("AuthenticationDatabase"),
pgOptions => pgOptions.EnableRetryOnFailure()
);
});
Connection String:
{
"ConnectionStrings": {
"AuthenticationDatabase": "Host=localhost;Database=sila2auth;Username=postgres;Password=yourpassword"
}
}
Add NuGet Package:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
Implement your own storage backend for MongoDB, Azure Cosmos DB, or any other technology:
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
// 1. Implement IUserRepository for your storage technology
public class MongoDbUserRepository : IUserRepository
{
private readonly IMongoCollection<User> _users;
public MongoDbUserRepository(IMongoDatabase database)
{
_users = database.GetCollection<User>("users");
}
public async Task<User> GetByLoginAsync(string login, CancellationToken cancellationToken = default)
{
return await _users.Find(u => u.Login == login).FirstOrDefaultAsync(cancellationToken);
}
// Implement other methods...
}
// 2. Implement IAuthenticationService (or reuse SqliteAuthenticationService)
public class MongoDbAuthenticationService : IAuthenticationService
{
private readonly IUserRepository _userRepository;
private readonly PasswordHashService _passwordHashService;
// Implement authentication logic...
}
// 3. Register custom implementations
builder.Services.AddSiLA2AuthenticationWithCustomImplementations<
MongoDbUserRepository,
MongoDbAuthenticationService>();
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
public class UserSetupService
{
private readonly IUserManager _userManager;
public UserSetupService(IUserManager userManager)
{
_userManager = userManager;
}
public async Task CreateLabUsers()
{
// Create admin user
var admin = await _userManager.CreateUserAsync(
login: "lab.admin@company.com",
password: "SecurePassword123!",
role: Role.Admin
);
Console.WriteLine($"Created admin: {admin.Login} (ID: {admin.Id})");
// Create standard user
var operator1 = await _userManager.CreateUserAsync(
login: "operator1@company.com",
password: "OperatorPass456!",
role: Role.Standard
);
Console.WriteLine($"Created operator: {operator1.Login}");
// Check if user already exists
if (await _userManager.UserExistsAsync("operator2@company.com"))
{
Console.WriteLine("User operator2@company.com already exists");
}
}
}
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
public class LoginService
{
private readonly IAuthenticationService _authService;
public LoginService(IAuthenticationService authService)
{
_authService = authService;
}
public async Task<bool> Login(string username, string password)
{
var result = await _authService.AuthenticateAsync(username, password);
if (result.IsAuthenticated)
{
Console.WriteLine($"Welcome, {result.User.Login}!");
Console.WriteLine($"Role: {result.User.Role}");
return true;
}
switch (result.FailureReason)
{
case AuthenticationFailureReason.UserNotFound:
Console.WriteLine("User not found");
break;
case AuthenticationFailureReason.InvalidPassword:
Console.WriteLine("Invalid password");
break;
case AuthenticationFailureReason.InternalError:
Console.WriteLine($"Error: {result.ErrorMessage}");
break;
}
return false;
}
}
public class RoleManagementService
{
private readonly IUserManager _userManager;
public RoleManagementService(IUserManager userManager)
{
_userManager = userManager;
}
public async Task PromoteToAdmin(string login)
{
var user = await _userManager.GetUserByLoginAsync(login);
if (user == null)
{
Console.WriteLine($"User {login} not found");
return;
}
await _userManager.UpdateUserRoleAsync(user.Id, Role.Admin);
Console.WriteLine($"User {login} promoted to Admin");
}
public async Task DemoteToStandard(Guid userId)
{
await _userManager.UpdateUserRoleAsync(userId, Role.Standard);
Console.WriteLine("User demoted to Standard role");
}
public async Task ListUsersByRole()
{
var users = await _userManager.GetAllUsersAsync();
var admins = users.Where(u => u.Role == Role.Admin);
var standardUsers = users.Where(u => u.Role == Role.Standard);
Console.WriteLine("Administrators:");
foreach (var admin in admins)
{
Console.WriteLine($" - {admin.Login}");
}
Console.WriteLine("\nStandard Users:");
foreach (var user in standardUsers)
{
Console.WriteLine($" - {user.Login}");
}
}
}
public class PasswordManagementService
{
private readonly IUserManager _userManager;
public PasswordManagementService(IUserManager userManager)
{
_userManager = userManager;
}
public async Task<bool> ChangeUserPassword(
Guid userId,
string currentPassword,
string newPassword)
{
// Validate new password strength
if (newPassword.Length < 8)
{
Console.WriteLine("Password must be at least 8 characters");
return false;
}
var success = await _userManager.UpdateUserPasswordAsync(
userId,
currentPassword,
newPassword
);
if (success)
{
Console.WriteLine("Password changed successfully");
}
else
{
Console.WriteLine("Password change failed (invalid current password)");
}
return success;
}
public async Task ResetPassword(string login, string newPassword)
{
var user = await _userManager.GetUserByLoginAsync(login);
if (user == null)
{
Console.WriteLine($"User {login} not found");
return;
}
// Admin reset - update password hash directly
var authService = new SqliteAuthenticationService(
userRepository,
logger
);
user.SetPasswordHash(authService.HashPassword(newPassword));
await _userManager.UpdateUserAsync(user);
Console.WriteLine($"Password reset for {login}");
}
}
public class UserQueryService
{
private readonly IUserManager _userManager;
public UserQueryService(IUserManager userManager)
{
_userManager = userManager;
}
public async Task QueryUsers()
{
// Get all users
var allUsers = await _userManager.GetAllUsersAsync();
Console.WriteLine($"Total users: {allUsers.Count()}");
// Find specific user
var user = await _userManager.GetUserByLoginAsync("admin");
if (user != null)
{
Console.WriteLine($"Found user: {user.Login}, Role: {user.Role}");
}
// Advanced queries using IQueryable
var recentUsers = _userManager.GetUsers()
.Where(u => u.CreatedAt > DateTime.UtcNow.AddDays(-7))
.OrderByDescending(u => u.CreatedAt)
.ToList();
Console.WriteLine($"\nUsers created in last 7 days: {recentUsers.Count}");
foreach (var recentUser in recentUsers)
{
Console.WriteLine($" - {recentUser.Login} (created {recentUser.CreatedAt:yyyy-MM-dd})");
}
}
}
using SiLA2.AspNetCore;
using SiLA2.Server;
using SiLA2.Authentication.Extensions;
using Microsoft.EntityFrameworkCore;
using MyFeatures.Services;
var builder = WebApplication.CreateBuilder(args);
// Register SiLA2 core services
builder.Services.AddSingleton<ISiLA2Server, SiLA2Server>();
builder.Services.AddGrpc();
// Register authentication with SQLite
builder.Services.AddSiLA2Authentication(options =>
{
options.UseSqlite("Data Source=users.db");
});
// Register your feature services
builder.Services.AddSingleton<MyFeatureService>();
builder.Services.AddSingleton<SecureFeatureService>();
var app = builder.Build();
// Initialize SiLA2 features
var siLA2Server = app.Services.GetRequiredService<ISiLA2Server>();
app.InitializeSiLA2Features(siLA2Server);
// Initialize authentication database
app.Services.EnsureAuthenticationDatabaseCreated();
// Map gRPC services
app.MapGrpcService<SiLAService>();
app.MapGrpcService<MyFeatureService>();
app.MapGrpcService<SecureFeatureService>();
app.Run();
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
using Grpc.Core;
public class SecureFeatureService : SecureFeature.SecureFeatureBase
{
private readonly IAuthenticationService _authService;
private readonly Feature _siLA2Feature;
public SecureFeatureService(
ISiLA2Server siLA2Server,
IAuthenticationService authService)
{
_siLA2Feature = siLA2Server.ReadFeature(
Path.Combine("Features", "SecureFeature-v1_0.sila.xml"));
_authService = authService;
}
public override async Task<Response> ExecuteSecureCommand(
Parameters request,
ServerCallContext context)
{
// Extract credentials from metadata
var metadata = context.RequestHeaders;
var username = metadata.GetValue("username");
var password = metadata.GetValue("password");
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
throw new RpcException(new Status(
StatusCode.Unauthenticated,
"Missing credentials"));
}
// Authenticate user
var authResult = await _authService.AuthenticateAsync(username, password);
if (!authResult.IsAuthenticated)
{
throw new RpcException(new Status(
StatusCode.Unauthenticated,
$"Authentication failed: {authResult.ErrorMessage}"));
}
// Check authorization
if (authResult.User.Role != Role.Admin)
{
throw new RpcException(new Status(
StatusCode.PermissionDenied,
"Admin role required for this operation"));
}
// Execute secure command logic
Console.WriteLine($"User {authResult.User.Login} executed secure command");
return new Response
{
Message = new String { Value = "Command executed successfully" }
};
}
}
using Grpc.Core;
using Grpc.Net.Client;
// Create channel
var channel = GrpcChannel.ForAddress("https://localhost:50051");
var client = new SecureFeature.SecureFeatureClient(channel);
// Add authentication metadata
var metadata = new Metadata
{
{ "username", "admin" },
{ "password", "Admin" }
};
// Make authenticated call
try
{
var response = await client.ExecuteSecureCommandAsync(
new Parameters(),
metadata
);
Console.WriteLine($"Success: {response.Message.Value}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
Console.WriteLine("Authentication failed");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.PermissionDenied)
{
Console.WriteLine("Permission denied");
}
For automatic authentication on every request, use an interceptor:
using Grpc.Core;
using Grpc.Core.Interceptors;
using SiLA2.Authentication.Interfaces;
public class AuthenticationInterceptor : Interceptor
{
private readonly IAuthenticationService _authService;
public AuthenticationInterceptor(IAuthenticationService authService)
{
_authService = authService;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
// Extract credentials
var username = context.RequestHeaders.GetValue("username");
var password = context.RequestHeaders.GetValue("password");
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
var result = await _authService.AuthenticateAsync(username, password);
if (!result.IsAuthenticated)
{
throw new RpcException(new Status(
StatusCode.Unauthenticated,
"Invalid credentials"));
}
// Store user in context for downstream services
context.GetHttpContext()?.Items.Add("User", result.User);
}
return await continuation(request, context);
}
}
// Register interceptor in Program.cs
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<AuthenticationInterceptor>();
});
SQLite is the default storage backend and requires no external database server:
builder.Services.AddSiLA2Authentication(options =>
{
options.UseSqlite("Data Source=users.db");
});
// Database file will be created automatically at users.db
app.Services.EnsureAuthenticationDatabaseCreated();
Advantages:
1. Install EF Core Tools:
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
2. Configure in Program.cs:
builder.Services.AddSiLA2Authentication(options =>
{
options.UseSqlServer(
builder.Configuration.GetConnectionString("AuthenticationDatabase"),
sqlOptions => sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null
)
);
});
3. Apply Migrations:
// Automatic migration on startup
app.Services.EnsureAuthenticationDatabaseCreated();
// Or use EF CLI manually
dotnet ef database update --context AuthenticationDbContext
Connection String (appsettings.json):
{
"ConnectionStrings": {
"AuthenticationDatabase": "Server=localhost;Database=SiLA2Auth;Integrated Security=true;TrustServerCertificate=true;"
}
}
1. Install PostgreSQL Provider:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
2. Configure in Program.cs:
builder.Services.AddSiLA2Authentication(options =>
{
options.UseNpgsql(
builder.Configuration.GetConnectionString("AuthenticationDatabase"),
pgOptions => pgOptions.EnableRetryOnFailure()
);
});
3. Create Database:
-- In PostgreSQL (psql)
CREATE DATABASE sila2auth;
CREATE USER sila2user WITH PASSWORD 'yourpassword';
GRANT ALL PRIVILEGES ON DATABASE sila2auth TO sila2user;
Connection String:
{
"ConnectionStrings": {
"AuthenticationDatabase": "Host=localhost;Database=sila2auth;Username=sila2user;Password=yourpassword"
}
}
The module uses DbUp for database migrations, which runs embedded SQL scripts automatically:
Embedded Migration Scripts:
001_CreateUserTable.sql - Creates User table with indexes002_SeedDefaultUsers.sql - Seeds admin and user accountsAdding Custom Migrations:
Migrations folder:-- 003_AddEmailColumn.sql
ALTER TABLE "User" ADD COLUMN "Email" TEXT;
.csproj:<ItemGroup>
<EmbeddedResource Include="Migrations/*.sql" />
</ItemGroup>
The module seeds two default user accounts for development:
| Login | Password | Role | GUID |
|---|---|---|---|
Admin |
Admin |
Admin | 66D6855A-CC4A-4BCD-B4AE-10CE3C59F1BD |
User |
User |
Standard | 59845510-06DF-4981-AA68-253400D3090C |
Security Warning:
These default credentials are for development and testing only. They use well-known passwords that are included in embedded SQL scripts.
In production environments, you MUST:
Changing Default Passwords:
var userManager = app.Services.GetRequiredService<IUserManager>();
// Change admin password
var admin = await userManager.GetUserByLoginAsync("Admin");
await userManager.UpdateUserPasswordAsync(
admin.Id,
"Admin", // old password
"SecureAdminPassword123!" // new password
);
// Or delete default accounts entirely
await userManager.DeleteUserAsync(admin.Id);
Implement IUserRepository for any storage technology:
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
using MongoDB.Driver;
public class MongoDbUserRepository : IUserRepository
{
private readonly IMongoCollection<User> _users;
public MongoDbUserRepository(IMongoDatabase database)
{
_users = database.GetCollection<User>("users");
}
public async Task<User> GetByIdAsync(Guid userId, CancellationToken cancellationToken = default)
{
return await _users.Find(u => u.Id == userId)
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<User> GetByLoginAsync(string login, CancellationToken cancellationToken = default)
{
return await _users.Find(u => u.Login == login)
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<IEnumerable<User>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _users.Find(_ => true).ToListAsync(cancellationToken);
}
public IQueryable<User> GetQueryable()
{
return _users.AsQueryable();
}
public async Task<User> CreateAsync(User user, CancellationToken cancellationToken = default)
{
await _users.InsertOneAsync(user, cancellationToken: cancellationToken);
return user;
}
public async Task<User> UpdateAsync(User user, CancellationToken cancellationToken = default)
{
await _users.ReplaceOneAsync(u => u.Id == user.Id, user, cancellationToken: cancellationToken);
return user;
}
public async Task DeleteAsync(Guid userId, CancellationToken cancellationToken = default)
{
await _users.DeleteOneAsync(u => u.Id == userId, cancellationToken);
}
public async Task<bool> ExistsAsync(string login, CancellationToken cancellationToken = default)
{
var count = await _users.CountDocumentsAsync(u => u.Login == login, cancellationToken: cancellationToken);
return count > 0;
}
}
// Register in DI
builder.Services.AddSingleton<IMongoDatabase>(sp =>
{
var client = new MongoClient("mongodb://localhost:27017");
return client.GetDatabase("sila2auth");
});
builder.Services.AddSiLA2AuthenticationWithCustomImplementations<
MongoDbUserRepository,
SqliteAuthenticationService>(); // Reuse authentication logic
Implement custom authentication logic (e.g., LDAP, OAuth):
using SiLA2.Authentication.Interfaces;
using SiLA2.Authentication.Models;
public class LdapAuthenticationService : IAuthenticationService
{
private readonly IUserRepository _userRepository;
private readonly ILdapConnection _ldapConnection;
public LdapAuthenticationService(
IUserRepository userRepository,
ILdapConnection ldapConnection)
{
_userRepository = userRepository;
_ldapConnection = ldapConnection;
}
public async Task<AuthenticationResult> AuthenticateAsync(
string login,
string password,
CancellationToken cancellationToken = default)
{
// Authenticate against LDAP
bool isLdapValid = await _ldapConnection.ValidateCredentialsAsync(login, password);
if (!isLdapValid)
{
return AuthenticationResult.Failure(
AuthenticationFailureReason.InvalidPassword,
"LDAP authentication failed");
}
// Get or create user in local database
var user = await _userRepository.GetByLoginAsync(login, cancellationToken);
if (user == null)
{
// Auto-provision user from LDAP
user = new User(login, string.Empty, Role.Standard);
user = await _userRepository.CreateAsync(user, cancellationToken);
}
return AuthenticationResult.Success(user);
}
// Implement other methods...
}
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SiLA2.Authentication.Interfaces;
public class JwtAuthenticationService : IAuthenticationService
{
private readonly IUserRepository _userRepository;
private readonly IConfiguration _configuration;
public async Task<AuthenticationResult> AuthenticateAsync(
string login,
string jwtToken, // Treat password parameter as JWT token
CancellationToken cancellationToken = default)
{
// Validate JWT token
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]))
};
try
{
var principal = tokenHandler.ValidateToken(
jwtToken,
validationParameters,
out var validatedToken);
// Extract user info from claims
var userClaim = principal.FindFirst(ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(userClaim))
{
return AuthenticationResult.Failure(
AuthenticationFailureReason.InvalidPassword,
"Invalid token claims");
}
// Get or create user
var user = await _userRepository.GetByLoginAsync(userClaim, cancellationToken);
return AuthenticationResult.Success(user);
}
catch (Exception)
{
return AuthenticationResult.Failure(
AuthenticationFailureReason.InvalidPassword,
"JWT validation failed");
}
}
}
// Basic authentication
Task<AuthenticationResult> AuthenticateAsync(
string login,
string password,
CancellationToken cancellationToken = default)
// Server-specific authentication
Task<AuthenticationResult> AuthenticateForServerAsync(
string login,
string password,
Guid requestedServerId,
Guid serverConfigId,
CancellationToken cancellationToken = default)
// Password management
bool ValidatePassword(string passwordHash, string password)
string HashPassword(string password)
Task<bool> ChangePasswordAsync(
Guid userId,
string currentPassword,
string newPassword,
CancellationToken cancellationToken = default)
// User creation and retrieval
Task<User> CreateUserAsync(string login, string password, Role role = Role.Standard, CancellationToken cancellationToken = default)
Task<User> GetUserByIdAsync(Guid userId, CancellationToken cancellationToken = default)
Task<User> GetUserByLoginAsync(string login, CancellationToken cancellationToken = default)
Task<IEnumerable<User>> GetAllUsersAsync(CancellationToken cancellationToken = default)
// User updates
Task<User> UpdateUserAsync(User user, CancellationToken cancellationToken = default)
Task<User> UpdateUserRoleAsync(Guid userId, Role newRole, CancellationToken cancellationToken = default)
Task<bool> UpdateUserPasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken = default)
// User deletion and queries
Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken = default)
Task<bool> UserExistsAsync(string login, CancellationToken cancellationToken = default)
IQueryable<User> GetUsers()
Client Request
|
v
[Extract Credentials from Metadata]
|
v
[IAuthenticationService.AuthenticateAsync()]
|
v
[IUserRepository.GetByLoginAsync()]
|
v
[PasswordHashService.Verify(hash, password)]
|
+--- Invalid --> AuthenticationResult.Failure()
|
+--- Valid --> AuthenticationResult.Success(user)
|
v
[Check Role]
|
+--- Admin --> Allow Full Access
|
+--- Standard --> Allow Limited Access
|
+--- Undefined --> Deny Access
SiLA2.Authentication has the following NuGet dependencies:
| Package | Version | Purpose |
|---|---|---|
dbup-sqlite |
6.0.4 | Database migration management |
Microsoft.EntityFrameworkCore |
10.0.2 | Data access abstraction layer |
Microsoft.EntityFrameworkCore.Sqlite |
10.0.2 | SQLite database provider |
SiLA2.Utils |
10.2.1+ | SiLA2 utility library (password hashing) |
Additional dependencies for other databases:
| Database | Package |
|---|---|
| SQL Server | Microsoft.EntityFrameworkCore.SqlServer 10.0.2+ |
| PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0+ |
| MySQL | Pomelo.EntityFrameworkCore.MySql 10.0.0+ |
Symptom: SQLite database file doesn't exist after calling EnsureAuthenticationDatabaseCreated()
Solution:
// Check if database creation was called
var migrated = app.Services.EnsureAuthenticationDatabaseCreated();
Console.WriteLine($"Database migrated: {migrated}");
// Verify connection string
var dbContext = app.Services.GetRequiredService<AuthenticationDbContext>();
Console.WriteLine($"Connection string: {dbContext.Database.GetConnectionString()}");
// Ensure directory exists
var dbPath = "users.db";
var directory = Path.GetDirectoryName(Path.GetFullPath(dbPath));
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
Symptom: Cannot login with "Admin" or "User" default accounts
Solution:
// Check if users exist
var userManager = app.Services.GetRequiredService<IUserManager>();
var admin = await userManager.GetUserByLoginAsync("Admin");
if (admin == null)
{
Console.WriteLine("Admin user not found - migration may have failed");
// Manually create admin user
await userManager.CreateUserAsync("Admin", "Admin", Role.Admin);
}
// Verify migration scripts are embedded
var assembly = typeof(ServiceCollectionExtensions).Assembly;
var resources = assembly.GetManifestResourceNames();
foreach (var resource in resources)
{
Console.WriteLine($"Embedded resource: {resource}");
}
Symptom: Valid credentials are rejected
Solution:
// Enable detailed logging
builder.Logging.SetMinimumLevel(LogLevel.Debug);
builder.Logging.AddConsole();
// Test password hashing
var authService = app.Services.GetRequiredService<IAuthenticationService>();
var hash = authService.HashPassword("Admin");
Console.WriteLine($"Password hash: {hash}");
var isValid = authService.ValidatePassword(hash, "Admin");
Console.WriteLine($"Password validation: {isValid}");
// Check user in database
var userManager = app.Services.GetRequiredService<IUserManager>();
var user = await userManager.GetUserByLoginAsync("Admin");
if (user != null)
{
Console.WriteLine($"User found: {user.Login}, Hash: {user.PasswordHash}");
}
Symptom: EnsureAuthenticationDatabaseCreated() throws exception with SQL Server
Solution:
DbUp migrations are currently designed for SQLite. For SQL Server, use Entity Framework migrations instead:
# Add migration
dotnet ef migrations add InitialCreate --context AuthenticationDbContext
# Apply migration
dotnet ef database update --context AuthenticationDbContext
Or manually create tables:
CREATE TABLE [User] (
[Id] UNIQUEIDENTIFIER PRIMARY KEY NOT NULL,
[Login] NVARCHAR(255) NOT NULL UNIQUE,
[PasswordHash] NVARCHAR(MAX) NOT NULL,
[Role] INT NOT NULL DEFAULT 1,
[CreatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
[UpdatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
CREATE INDEX IX_User_Login ON [User] ([Login]);
Symptom: Exception: "A relational database connection string was not found"
Solution:
// Verify appsettings.json is copied to output
// Check .csproj has:
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
// Or configure connection string in code
builder.Services.AddSiLA2Authentication(options =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? "Data Source=users.db";
options.UseSqlite(connectionString);
});
{salt};{hash} (Base64 encoded)// ✅ GOOD: Use secure password
var user = await userManager.CreateUserAsync(
"admin@company.com",
"MySecure!Password123", // Strong password
Role.Admin
);
// ❌ BAD: Weak password
var user = await userManager.CreateUserAsync(
"admin",
"admin", // Easily guessed
Role.Admin
);
// ✅ GOOD: Store in user secrets or environment variables
builder.Configuration.AddUserSecrets<Program>();
var connectionString = builder.Configuration.GetConnectionString("AuthenticationDatabase");
// ❌ BAD: Hardcoded credentials
var connectionString = "Server=localhost;Database=SiLA2Auth;User=sa;Password=password123";
src/Examples/AuthenticationAuthorization/This library is part of the SiLA2 C# implementation. Contributions are welcome!
For issues and feature requests, visit: https://gitlab.com/SiLA2/sila_csharp/-/issues
This project is licensed under the MIT License. See the repository for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 1 NuGet packages that depend on SiLA2.Authentication:
| Package | Downloads |
|---|---|
|
SiLA2.Frontend.Razor
Web Frontend Extension for SiLA2.Server Package |
This package is not used by any popular GitHub repositories.