User Management¶
Flaggy includes built-in user management with secure authentication for the dashboard. Users are stored in the database with BCrypt-hashed passwords, ensuring secure access to your feature flag management interface.
Overview¶
User management features include:
- Secure Authentication: BCrypt password hashing with work factor 12
- Database-Backed Storage: Users stored alongside feature flags
- User CRUD Operations: Full create, read, update, delete support
- Session Management: Cookie-based authentication
- Multi-Provider Support: Works with all database providers
- Programmatic API: Manage users through code or dashboard UI
User Model¶
Each user has the following properties:
public class User
{
public string username { get; set; } // Unique identifier, primary key
public string password_hash { get; set; } // BCrypt hashed password
public string? email { get; set; } // Optional email address
public DateTime created_at { get; set; } // User creation timestamp
}
Database Schema¶
The users table is automatically created during migration:
MySQL¶
CREATE TABLE users (
username VARCHAR(255) PRIMARY KEY,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(255) NULL,
created_at DATETIME NULL
);
PostgreSQL¶
CREATE TABLE users (
username VARCHAR(255) PRIMARY KEY,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(255) NULL,
created_at TIMESTAMP NULL
);
MS SQL Server¶
CREATE TABLE users (
username NVARCHAR(255) PRIMARY KEY,
password_hash NVARCHAR(255) NOT NULL,
email NVARCHAR(255) NULL,
created_at DATETIME NULL
);
The user table is created automatically when using database providers with auto-migration enabled.
Configuration¶
Enabling Authentication¶
Authentication is enabled by default:
using Flaggy.Extensions;
using Flaggy.UI.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Configure Flaggy with MySQL (creates user table automatically)
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;",
autoMigrate: true // Creates both feature_flags and users tables
);
});
var app = builder.Build();
// Dashboard with authentication (default)
app.UseFlaggyUI(options =>
{
options.EnableAuthentication = true; // Default: true
});
app.Run();
Disabling Authentication¶
For development or internal tools only:
app.UseFlaggyUI(options =>
{
options.EnableAuthentication = false; // Disable authentication
});
Warning: Never disable authentication in production environments!
Default Credentials¶
If no users exist in the database, Flaggy falls back to default credentials:
- username:
admin - Password:
admin
Important: The default credentials are only used as a fallback. Once you create users through the dashboard, the default credentials are disabled.
Security Best Practice¶
- On first login with default credentials
- Create a new admin user with a strong password
- The default admin fallback is automatically disabled once real users exist
Dashboard User Management¶
Accessing User Management¶
Navigate to {RoutePrefix}/users (e.g., https://localhost:5001/flaggy/users)
The user management interface provides:
- List all users
- Create new users
- Update existing users
- Delete users
- Search and filter users
Creating a User¶
- Click "Create User" button
- Enter username (required, unique)
- Enter password (required, min 8 characters recommended)
- Enter email (optional)
- Click "Save"
The password is automatically hashed using BCrypt before storage.
Updating a User¶
- Click "Edit" on the user row
- Update password (leave blank to keep current)
- Update email
- Click "Save"
Deleting a User¶
- Click "Delete" on the user row
- Confirm deletion
- User is permanently removed
Warning: Cannot delete the last remaining user.
Programmatic User Management¶
IUserProvider Interface¶
All user operations go through the IUserProvider interface:
public interface IUserProvider
{
Task<User?> GetUserAsync(string username, CancellationToken cancellationToken = default);
Task<IEnumerable<User>> GetAllUsersAsync(CancellationToken cancellationToken = default);
Task<bool> CreateUserAsync(User user, CancellationToken cancellationToken = default);
Task<bool> UpdateUserAsync(User user, CancellationToken cancellationToken = default);
Task<bool> DeleteUserAsync(string username, CancellationToken cancellationToken = default);
Task<bool> UserExistsAsync(string username, CancellationToken cancellationToken = default);
}
Creating Users Programmatically¶
using Flaggy.Abstractions;
using Flaggy.Models;
using Flaggy.Services;
public class UserManagementService
{
private readonly IUserProvider _userProvider;
public UserManagementService(IUserProvider userProvider)
{
_userProvider = userProvider;
}
public async Task CreateAdminUser()
{
var user = new User
{
username = "admin",
password_hash = password_hasher.HashPassword("SecurePassword123!"),
email = "admin@example.com",
created_at = DateTime.UtcNow
};
var created = await _userProvider.CreateUserAsync(user);
if (created)
{
Console.WriteLine("Admin user created successfully");
}
}
}
Checking User Credentials¶
using Flaggy.Services;
public async Task<bool> ValidateLogin(string username, string password)
{
var user = await _userProvider.GetUserAsync(username);
if (user == null)
{
return false;
}
return password_hasher.VerifyPassword(password, user.password_hash);
}
Listing All Users¶
public async Task ListAllUsers()
{
var users = await _userProvider.GetAllUsersAsync();
foreach (var user in users)
{
Console.WriteLine($"username: {user.username}, email: {user.email}, Created: {user.created_at}");
}
}
Updating User Password¶
public async Task UpdateUserPassword(string username, string newPassword)
{
var user = await _userProvider.GetUserAsync(username);
if (user == null)
{
throw new Exception("User not found");
}
user.password_hash = password_hasher.HashPassword(newPassword);
var updated = await _userProvider.UpdateUserAsync(user);
if (updated)
{
Console.WriteLine("Password updated successfully");
}
}
Deleting a User¶
public async Task DeleteUser(string username)
{
var deleted = await _userProvider.DeleteUserAsync(username);
if (deleted)
{
Console.WriteLine($"User {username} deleted successfully");
}
}
Seeding Initial Users¶
At Application Startup¶
using Flaggy.Abstractions;
using Flaggy.Models;
using Flaggy.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: builder.Configuration.GetConnectionString("FlaggyDb"
);
})
);
var app = builder.Build();
// Seed initial admin user
using (var scope = app.Services.CreateScope())
{
var userProvider = scope.ServiceProvider.GetRequiredService<IUserProvider>();
// Check if admin exists
var adminExists = await userProvider.UserExistsAsync("admin");
if (!adminExists)
{
var admin = new User
{
username = "admin",
password_hash = password_hasher.HashPassword("YourSecurePassword123!"),
email = "admin@yourcompany.com",
created_at = DateTime.UtcNow
};
await userProvider.CreateUserAsync(admin);
}
}
app.UseFlaggyUI();
app.Run();
Multiple Users¶
var users = new[]
{
new User
{
username = "admin",
password_hash = password_hasher.HashPassword("AdminPass123!"),
email = "admin@company.com",
created_at = DateTime.UtcNow
},
new User
{
username = "developer",
password_hash = password_hasher.HashPassword("DevPass123!"),
email = "dev@company.com",
created_at = DateTime.UtcNow
},
new User
{
username = "operator",
password_hash = password_hasher.HashPassword("OpPass123!"),
email = "ops@company.com",
created_at = DateTime.UtcNow
}
};
foreach (var user in users)
{
var exists = await userProvider.UserExistsAsync(user.username);
if (!exists)
{
await userProvider.CreateUserAsync(user);
}
}
Password Security¶
BCrypt Hashing¶
Flaggy uses BCrypt for password hashing with a work factor of 12:
public static class password_hasher
{
private const int WorkFactor = 12;
public static string HashPassword(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password, WorkFactor);
}
public static bool VerifyPassword(string password, string hash)
{
try
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
catch
{
return false;
}
}
}
Key Features: - Work factor 12 (recommended for production) - Automatic salt generation - Resistant to rainbow table attacks - Future-proof (cost increases over time)
Password Best Practices¶
- Minimum Length: Enforce at least 12 characters
- Complexity: Require uppercase, lowercase, numbers, and symbols
- No Dictionary Words: Avoid common words
- Unique Passwords: Different from other accounts
- Regular Updates: Change passwords periodically
Example password validation:
public static bool ValidatePassword(string password)
{
if (password.Length < 12)
return false;
bool hasUpper = password.Any(char.IsUpper);
bool hasLower = password.Any(char.IsLower);
bool hasDigit = password.Any(char.IsDigit);
bool hasSpecial = password.Any(c => !char.IsLetterOrDigit(c));
return hasUpper && hasLower && hasDigit && hasSpecial;
}
Authentication Flow¶
Login Process¶
- User submits username and password
- System retrieves user from database
- Password is verified against BCrypt hash
- On success, authentication cookie is created
- Cookie contains Base64-encoded token with username and timestamp
- User is redirected to dashboard
Token Format¶
Base64("FlaggyAuth:username:timestamp")
Example:
RmxhZ2d5QXV0aDphZG1pbjoxNzM3MjExMjAw
Session Management¶
private string GenerateAuthToken(string username)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var token = $"FlaggyAuth:{username}:{timestamp}";
return Convert.ToBase64String(Encoding.UTF8.GetBytes(token));
}
Cookie Configuration¶
- Name:
FlaggyAuth - HttpOnly: true (prevents JavaScript access)
- Secure: true (HTTPS only in production)
- SameSite: Strict (CSRF protection)
- Expiration: Session (browser close)
Logout Process¶
- User clicks logout
- Authentication cookie is cleared
- User is redirected to login page
User Providers by Platform¶
MySQL User Provider¶
using Flaggy.Extensions;
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;",
userTableName: "users" // Optional, default is "users"
);
});
PostgreSQL User Provider¶
using Flaggy.Extensions;
builder.Services.AddFlaggy(options =>
{
options.UsePostgreSQL(
connectionString: "Host=localhost;Database=myapp;username=postgres;Password=pass",
userTableName: "users" // Optional, default is "users"
);
});
MS SQL Server User Provider¶
using Flaggy.Extensions;
builder.Services.AddFlaggy(options =>
{
options.UseMsSql(
connectionString: "Server=localhost;Database=myapp;User Id=sa;Password=pass;TrustServerCertificate=True",
userTableName: "users" // Optional, default is "users"
);
});
InMemory User Provider¶
The InMemory provider includes a user provider that stores users in memory:
using Flaggy.Extensions;
using Flaggy.Providers;
builder.Services.AddFlaggy(new InMemoryFeatureFlagProvider());
// Users are stored in memory (not persisted)
// Suitable for development/testing only
Advanced Scenarios¶
Custom User Provider¶
Implement IUserProvider for custom storage:
using Flaggy.Abstractions;
using Flaggy.Models;
public class CustomUserProvider : IUserProvider
{
private readonly IYourCustomDatabase _database;
public CustomUserProvider(IYourCustomDatabase database)
{
_database = database;
}
public async Task<User?> GetUserAsync(string username, CancellationToken cancellationToken = default)
{
return await _database.Users.FindAsync(username);
}
public async Task<IEnumerable<User>> GetAllUsersAsync(CancellationToken cancellationToken = default)
{
return await _database.Users.ToListAsync();
}
public async Task<bool> CreateUserAsync(User user, CancellationToken cancellationToken = default)
{
await _database.Users.AddAsync(user);
return await _database.SaveChangesAsync() > 0;
}
public async Task<bool> UpdateUserAsync(User user, CancellationToken cancellationToken = default)
{
_database.Users.Update(user);
return await _database.SaveChangesAsync() > 0;
}
public async Task<bool> DeleteUserAsync(string username, CancellationToken cancellationToken = default)
{
var user = await GetUserAsync(username);
if (user != null)
{
_database.Users.Remove(user);
return await _database.SaveChangesAsync() > 0;
}
return false;
}
public async Task<bool> UserExistsAsync(string username, CancellationToken cancellationToken = default)
{
return await _database.Users.AnyAsync(u => u.username == username);
}
}
Integration with External Authentication¶
Use custom authorization filters to integrate with existing auth:
app.UseFlaggyUI(options =>
{
options.EnableAuthentication = true;
options.AuthorizationFilter = context =>
{
// Check your existing authentication system
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
return false;
// Check if user has permission
var hasPermission = context.User.IsInRole("FlagAdmin") ||
context.User.HasClaim("Permission", "ManageFlags");
return hasPermission;
};
});
Audit Logging¶
Track user actions:
public class AuditedUserProvider : IUserProvider
{
private readonly IUserProvider _innerProvider;
private readonly ILogger<AuditedUserProvider> _logger;
public AuditedUserProvider(IUserProvider innerProvider, ILogger<AuditedUserProvider> logger)
{
_innerProvider = innerProvider;
_logger = logger;
}
public async Task<bool> CreateUserAsync(User user, CancellationToken cancellationToken = default)
{
var result = await _innerProvider.CreateUserAsync(user, cancellationToken);
if (result)
{
_logger.LogInformation("User created: {username} at {Time}", user.username, DateTime.UtcNow);
}
return result;
}
public async Task<bool> DeleteUserAsync(string username, CancellationToken cancellationToken = default)
{
var result = await _innerProvider.DeleteUserAsync(username, cancellationToken);
if (result)
{
_logger.LogWarning("User deleted: {username} at {Time}", username, DateTime.UtcNow);
}
return result;
}
// Implement other methods similarly...
}
Security Best Practices¶
1. Strong Password Policy¶
public static string ValidatePasswordStrength(string password)
{
if (password.Length < 12)
return "Password must be at least 12 characters long";
if (!password.Any(char.IsUpper))
return "Password must contain at least one uppercase letter";
if (!password.Any(char.IsLower))
return "Password must contain at least one lowercase letter";
if (!password.Any(char.IsDigit))
return "Password must contain at least one digit";
if (!password.Any(c => !char.IsLetterOrDigit(c)))
return "Password must contain at least one special character";
return string.Empty; // Valid
}
2. Account Lockout¶
Implement account lockout after failed attempts:
// Track failed login attempts (use distributed cache for load-balanced apps)
private readonly Dictionary<string, int> _failedAttempts = new();
public async Task<bool> AttemptLogin(string username, string password)
{
// Check if account is locked
if (_failedAttempts.TryGetValue(username, out var attempts) && attempts >= 5)
{
_logger.LogWarning("Account locked: {username}", username);
return false;
}
var user = await _userProvider.GetUserAsync(username);
if (user == null || !password_hasher.VerifyPassword(password, user.password_hash))
{
// Increment failed attempts
_failedAttempts[username] = attempts + 1;
return false;
}
// Reset failed attempts on successful login
_failedAttempts.Remove(username);
return true;
}
3. Password Expiration¶
Implement password expiration:
public class User
{
public string username { get; set; }
public string password_hash { get; set; }
public string? email { get; set; }
public DateTime created_at { get; set; }
public DateTime PasswordChangedAt { get; set; } // Add this field
}
public bool IsPasswordExpired(User user, int expirationDays = 90)
{
return (DateTime.UtcNow - user.PasswordChangedAt).TotalDays > expirationDays;
}
4. Two-Factor Authentication (2FA)¶
Consider adding 2FA for additional security:
// This would require extending the User model and authentication flow
public class User
{
public string username { get; set; }
public string password_hash { get; set; }
public string? email { get; set; }
public DateTime created_at { get; set; }
public bool TwoFactorEnabled { get; set; }
public string? TwoFactorSecret { get; set; }
}
5. Secure Password Reset¶
Implement secure password reset with time-limited tokens:
public class PasswordResetToken
{
public string username { get; set; }
public string Token { get; set; }
public DateTime ExpiresAt { get; set; }
}
public string GenerateResetToken(string username)
{
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
// Store token with expiration (e.g., 1 hour)
return token;
}
Troubleshooting¶
Cannot Login with Default Credentials¶
Problem: Default admin/admin credentials don't work.
Solution: Default credentials are disabled once users exist in the database. Create a new user or reset the database.
Password Not Updating¶
Problem: Password update doesn't take effect.
Solution: Ensure you're hashing the password:
user.password_hash = password_hasher.HashPassword(newPassword);
await _userProvider.UpdateUserAsync(user);
Users Table Not Created¶
Problem: Users table doesn't exist.
Solution: Enable auto-migration:
builder.Services.AddFlaggy(options =>
{
options.UseMySQL(
connectionString: connectionString,
autoMigrate: true // Enables automatic table creation
);
});
Authentication Cookie Not Persisting¶
Problem: Users logged out after every request.
Solution: Ensure HTTPS in production and check cookie settings:
builder.Services.AddHttpsRedirection(options =>
{
options.HttpsPort = 443;
});
Related Topics¶
- Dashboard - Using the dashboard interface
- Auto Migration - Understanding user table creation
- Providers - Database provider configuration
- Programmatic API - Managing flags and users via code