SiLA2.Authentication 10.2.2

dotnet add package SiLA2.Authentication --version 10.2.2
                    
NuGet\Install-Package SiLA2.Authentication -Version 10.2.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SiLA2.Authentication" Version="10.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SiLA2.Authentication" Version="10.2.2" />
                    
Directory.Packages.props
<PackageReference Include="SiLA2.Authentication" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SiLA2.Authentication --version 10.2.2
                    
#r "nuget: SiLA2.Authentication, 10.2.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SiLA2.Authentication@10.2.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SiLA2.Authentication&version=10.2.2
                    
Install as a Cake Addin
#tool nuget:?package=SiLA2.Authentication&version=10.2.2
                    
Install as a Cake Tool

SiLA2.Authentication

User Authentication and Role-Based Authorization for SiLA2 .NET Implementation

NuGet Package SiLA2.Authentication on NuGet.org
Repository https://gitlab.com/SiLA2/sila_csharp
SiLA Standard https://sila-standard.com
License MIT

Overview

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.

Key Features

  • Database-Agnostic Design - Interface-based architecture supports any storage backend
  • Default SQLite Implementation - Production-ready implementation with Entity Framework Core
  • BCrypt Password Hashing - Industry-standard password security (never stores plain text)
  • Role-Based Authorization - Admin, Standard, and Undefined user roles
  • User Management - Complete CRUD operations for user accounts
  • Automatic Database Migration - DbUp-based migration system with embedded SQL scripts
  • Default User Seeding - Pre-configured admin and user accounts for development
  • Server-Specific Authentication - Validate users against specific server instances
  • Dependency Injection Ready - Seamless ASP.NET Core integration

Relationship to SiLA2.Core

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:

  • Multi-user laboratory environments requiring access control
  • Compliance requirements (FDA 21 CFR Part 11, EU Annex 11)
  • Production deployments where user accountability is needed
  • Systems requiring different permission levels (admin vs. standard users)

Installation

Install via NuGet Package Manager:

dotnet add package SiLA2.Authentication

Or via Package Manager Console:

Install-Package SiLA2.Authentication

Prerequisites

  • .NET 10.0 or later
  • SiLA2.Utils (automatically installed as dependency)
  • Microsoft.EntityFrameworkCore 10.0.2+ (for SQLite implementation)
  • dbup-sqlite 6.0.4+ (for database migrations)

Quick Start

Get authentication working in your SiLA2 server in 3 steps:

1. Add the Package

dotnet add package SiLA2.Authentication

2. Register Services in Program.cs

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();

3. Initialize Database on Startup

// Initialize authentication database (creates tables and seeds default users)
app.Services.EnsureAuthenticationDatabaseCreated();

// Map gRPC services and start server...
app.MapGrpcService<MySiLA2Service>();
app.Run();

4. Use in Your Services

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:

  • Admin account (login: "Admin", password: "Admin")
  • Standard user account (login: "User", password: "User")
  • SQLite database with proper password hashing

Security Warning: Default credentials are for development only. Change them immediately in production!


Core Components

Interfaces

The module provides a clean separation between interface contracts and implementations:

IAuthenticationService

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);
}
IUserManager

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();
}
IUserRepository

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);
}
IAuthenticationInspector

Validates authentication requests with server identity checking.

public interface IAuthenticationInspector
{
    Task<bool> IsAuthenticatedAsync(
        string userIdentification,
        string password,
        Guid requestedServerId,
        CancellationToken cancellationToken = default);
}

Models

User

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; }
}
Role

User roles for authorization.

[Flags]
public enum Role
{
    Admin = 0,      // Full access
    Standard = 1,   // Limited access
    Undefined = 2   // No defined role
}
AuthenticationResult

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
}

Default Implementation

The module includes a production-ready SQLite implementation:

  • SqliteAuthenticationService - BCrypt-based authentication
  • SqliteUserRepository - Entity Framework Core repository
  • UserManager - High-level user management
  • AuthenticationInspector - Server identity validation

Dependency Injection Setup

Default SQLite Setup

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();

SQL Server Configuration

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;"
  }
}

PostgreSQL Configuration

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

Custom Implementation Pattern

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>();

Usage Examples

Creating Users

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");
        }
    }
}

Authenticating Users

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;
    }
}

Managing User Roles

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}");
        }
    }
}

Password Changes

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}");
    }
}

User Queries

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})");
        }
    }
}

Integration with SiLA2 Servers

Complete Server Example

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();

Feature Service with Authentication

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" }
        };
    }
}

Client-Side Metadata Example

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");
}

gRPC Interceptor Integration

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>();
});

Database Setup

SQLite (Default)

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:

  • Zero configuration
  • Single file database
  • Perfect for development and embedded systems
  • Cross-platform

SQL Server Configuration

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;"
  }
}

PostgreSQL Configuration

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"
  }
}

Migration Management with DbUp

The module uses DbUp for database migrations, which runs embedded SQL scripts automatically:

Embedded Migration Scripts:

  • 001_CreateUserTable.sql - Creates User table with indexes
  • 002_SeedDefaultUsers.sql - Seeds admin and user accounts

Adding Custom Migrations:

  1. Create a new SQL file in Migrations folder:
-- 003_AddEmailColumn.sql
ALTER TABLE "User" ADD COLUMN "Email" TEXT;
  1. Mark as embedded resource in .csproj:
<ItemGroup>
  <EmbeddedResource Include="Migrations/*.sql" />
</ItemGroup>
  1. DbUp will automatically detect and run the new migration on next startup.

Default Users

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:

  1. Change default passwords immediately after first deployment
  2. Delete default accounts and create secure user accounts
  3. Use strong passwords (minimum 12 characters, mixed case, numbers, special characters)
  4. Consider external authentication providers (OAuth, LDAP, Active Directory)
  5. Enable audit logging for authentication events (see SiLA2.Audit module)

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);

Advanced Usage

Custom Repository Implementation

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

Custom Authentication Service

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...
}

Integration with External Authentication Providers

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");
        }
    }
}

API Reference

Key Interface Methods

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 management
bool ValidatePassword(string passwordHash, string password)
string HashPassword(string password)
Task<bool> ChangePasswordAsync(
    Guid userId,
    string currentPassword,
    string newPassword,
    CancellationToken cancellationToken = default)
IUserManager
// 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()

Authentication Workflow

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

Dependencies

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.2+ 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+

Platform Support

  • Target Framework: .NET 10.0
  • Operating Systems: Windows, Linux, macOS
  • Architecture: Platform-independent (AnyCPU)
  • Embedded Systems: Fully supported (SQLite is ideal for embedded deployments)

Troubleshooting

Issue: Database File Not Created

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);
}

Issue: Default Users Not Seeded

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}");
}

Issue: Authentication Always Fails

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}");
}

Issue: Migration Errors with SQL Server

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]);

Issue: Connection String Not Found

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);
});

Security Best Practices

Password Security

  • Passwords are never stored in plain text - always hashed with BCrypt
  • BCrypt includes automatic salt generation (unique per password)
  • Hash format: {salt};{hash} (Base64 encoded)
  • Verification is constant-time to prevent timing attacks

Secure Storage

// ✅ 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
);

Connection String Security

// ✅ 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";

Production Checklist

  • Change all default passwords
  • Delete or disable default user accounts
  • Use strong password policy (12+ characters, mixed case, numbers, symbols)
  • Store connection strings in secure configuration (Azure Key Vault, AWS Secrets Manager, user secrets)
  • Enable HTTPS/TLS for all gRPC communication
  • Use SQL Server or PostgreSQL (not SQLite) for production
  • Enable database backups
  • Integrate with SiLA2.Audit for authentication logging
  • Consider external authentication (OAuth, LDAP, Active Directory)
  • Implement account lockout after failed attempts
  • Enable multi-factor authentication (MFA) if required

Additional Resources

  • SiLA2.Audit - Track authentication events and user actions for compliance
  • SiLA2.Utils - Core utilities including password hashing (PasswordHashService)
  • SiLA2.Database.SQL - Additional SQL database utilities

External Documentation


Contributing

This library is part of the SiLA2 C# implementation. Contributions are welcome!

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add unit tests
  5. Submit a merge request

For issues and feature requests, visit: https://gitlab.com/SiLA2/sila_csharp/-/issues


License

This project is licensed under the MIT License. See the repository for details.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on SiLA2.Authentication:

Package Downloads
SiLA2.Frontend.Razor

Web Frontend Extension for SiLA2.Server Package

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.2.2 91 2/12/2026
10.2.1 103 1/25/2026
10.2.0 191 12/23/2025
10.1.0 168 12/13/2025