BMS POS uses a secure PIN-based authentication system with BCrypt password hashing, automatic legacy PIN upgrades, and session-based access control.
PIN-based authentication
Employees authenticate using their Employee ID and a numeric PIN code. The system supports three user roles:
Cashier - Access to point-of-sale operations
Inventory - Access to inventory management dashboard
Manager - Full access to all system features and settings
Login flow
The authentication process follows these steps:
Employee identification - User enters their Employee ID
PIN entry - User enters their secure PIN (masked as dots)
Role selection - User selects their role (Cashier, Inventory, or Manager)
Validation - System verifies credentials and role assignment
Session creation - Secure session is established upon successful login
Navigation - User is directed to their role-specific dashboard
// Authentication implementation
const login = async () => {
if ( ! employeeId || ! pin ) {
setStatusMessage ( 'Please enter both Employee ID and PIN' )
return
}
try {
setStatusMessage ( 'Validating credentials...' )
// Validate credentials via API
let result
if ( window . electronAPI ?. validateLogin ) {
result = await window . electronAPI . validateLogin ( employeeId , pin , selectedRole )
} else {
result = await ApiClient . postJson ( '/auth/login' , { employeeId , pin , selectedRole }, false )
}
if ( result . success && result . data ?. employee ) {
const employeeRole = result . data . employee . role ||
( result . data . employee . isManager ? 'Manager' : 'Cashier' )
// Create secure session
await SessionManager . createSession ({
id: result . data . employee . id ,
employeeId: result . data . employee . employeeId ,
name: result . data . employee . name ,
role: employeeRole ,
isManager: result . data . employee . isManager || employeeRole === 'Manager'
})
// Navigate based on role
switch ( employeeRole ) {
case 'Manager' :
navigate ( '/manager' )
break
case 'Cashier' :
navigate ( '/manager' )
break
case 'Inventory' :
navigate ( '/inventory-dashboard' )
break
}
}
} catch ( error ) {
console . error ( 'Login error:' , error )
alert ( 'Login Failed! Please check your connection and try again.' )
}
}
The login interface uses a numeric keypad optimized for fast PIN entry in retail environments.
BCrypt password hashing
Employee PINs are secured using BCrypt, an industry-standard password hashing algorithm.
Hash generation
The PinSecurityService generates secure hashes with a work factor of 12:
PinSecurityService.cs:14-28
private const int WorkFactor = 12 ; // BCrypt work factor (cost)
public string HashPin ( string plainTextPin )
{
if ( string . IsNullOrWhiteSpace ( plainTextPin ))
throw new ArgumentException ( "PIN cannot be null or empty" , nameof ( plainTextPin ));
// BCrypt automatically generates salt and includes it in the hash
return BCrypt . Net . BCrypt . HashPassword ( plainTextPin , WorkFactor );
}
PIN verification
PIN verification uses constant-time comparison to prevent timing attacks:
PinSecurityService.cs:30-51
public bool VerifyPin ( string plainTextPin , string hashedPin )
{
if ( string . IsNullOrWhiteSpace ( plainTextPin ) || string . IsNullOrWhiteSpace ( hashedPin ))
return false ;
try
{
return BCrypt . Net . BCrypt . Verify ( plainTextPin , hashedPin );
}
catch ( Exception ex )
{
Console . WriteLine ( $"PIN verification error: { ex . Message } " );
return false ;
}
}
BCrypt’s work factor of 12 provides strong protection against brute-force attacks while maintaining acceptable performance for authentication.
Legacy PIN upgrade
The system automatically upgrades plaintext PINs to hashed versions on first login:
AuthController.cs:189-211
private bool IsValidPin ( string storedPin , string providedPin )
{
// Check if stored PIN is legacy (plaintext)
if ( _pinSecurityService . IsLegacyPin ( storedPin ))
{
// Legacy plaintext comparison
bool isValid = storedPin == providedPin ;
// If valid, upgrade to hashed PIN in background
if ( isValid )
{
_ = Task . Run ( async () => await UpgradeLegacyPinAsync ( storedPin , providedPin ));
}
return isValid ;
}
else
{
// Modern hashed PIN verification
return _pinSecurityService . VerifyPin ( providedPin , storedPin );
}
}
Legacy PINs are identified by their format - BCrypt hashes always start with $2 followed by version information:
PinSecurityService.cs:53-67
public bool IsLegacyPin ( string pin )
{
if ( string . IsNullOrWhiteSpace ( pin ))
return false ;
// BCrypt hashes have format: $2a$12$... or $2b$12$...
// If it doesn't start with $2, it's plaintext (legacy)
return ! pin . StartsWith ( "$2" );
}
Session management
BMS POS uses session-based authentication to maintain user context throughout the application.
Session creation
When a user successfully logs in, the system creates a session containing:
Employee ID and database ID
Employee name
Assigned role (Cashier, Inventory, or Manager)
Manager status flag
Session timestamp
// Create secure session
await SessionManager . createSession ({
id: result . data . employee . id ,
employeeId: result . data . employee . employeeId ,
name: result . data . employee . name ,
role: employeeRole ,
isManager: result . data . employee . isManager || employeeRole === 'Manager'
})
Session storage
Session tokens are stored in browser localStorage, which is appropriate for the desktop kiosk deployment model where physical device security is assumed.
Auto-logout
The system supports configurable auto-logout after a period of inactivity to prevent unauthorized access when employees step away from the terminal.
Role validation
The authentication system validates that users log in with their assigned role:
// Check role validation if selectedRole is provided
if ( ! string . IsNullOrEmpty ( request . SelectedRole ))
{
var employeeRole = employee . Role ?? ( employee . IsManager ? "Manager" : "Cashier" );
if ( ! employeeRole . Equals ( request . SelectedRole , StringComparison . OrdinalIgnoreCase ))
{
// Log failed login attempt due to role mismatch
await LogFailedLoginAttempt (
request . EmployeeId ,
$"Role mismatch - Employee: { employeeRole } , Selected: { request . SelectedRole } " ,
employee . Id
);
return Unauthorized ( ApiResponse < LoginResponse >. ErrorResponse (
$"You are registered as a { employeeRole } . Please select ' { employeeRole } ' and try again." ,
AuthErrorCodes . ROLE_MISMATCH
));
}
}
Role mismatch attempts are logged as failed authentication events for security auditing.
Failed login tracking
All failed login attempts are logged with details for security monitoring:
AuthController.cs:243-267
private async Task LogFailedLoginAttempt ( string employeeId , string reason , int ? employeeDbId )
{
try
{
await _userActivityService . LogActivityAsync (
null , // No valid user ID for failed attempts
employeeId ,
$"Failed login attempt for employee ID: { employeeId } " ,
reason ,
"Employee" ,
employeeDbId ,
"LOGIN_FAILED" ,
HttpContext . Connection ? . RemoteIpAddress ? . ToString ()
);
}
catch ( Exception ex )
{
Console . WriteLine ( $"Error logging failed login: { ex . Message } " );
// Don't throw - logging failure shouldn't break authentication
}
}
Failed login attempts are tracked for these scenarios:
Employee not found - Invalid Employee ID
Invalid PIN - Incorrect PIN for valid Employee ID
Role mismatch - User selected wrong role for their account
Account inactive - Account has been deactivated
Manager PIN validation
Sensitive operations require manager approval through a secondary PIN validation:
AuthController.cs:158-185
[ HttpPost ( "validate-manager" )]
public async Task < ActionResult < ValidateManagerResponse >> ValidateManager ( ValidateManagerRequest request )
{
// Find managers and verify PIN with hashing support
var managers = await _context . Employees
. Where ( e => ( e . Role == "Manager" || e . IsManager == true ) && e . IsActive )
. ToListAsync ();
// Check PIN against all managers (supports both legacy and hashed PINs)
var manager = managers . FirstOrDefault ( m => IsValidPin ( m . Pin , request . Pin ));
if ( manager == null )
{
return Ok ( new ValidateManagerResponse
{
Success = false ,
Message = "Invalid manager PIN"
});
}
return Ok ( new ValidateManagerResponse
{
Success = true ,
Message = "Manager PIN validated successfully" ,
ManagerName = manager . Name
});
}
This feature is used for operations like:
Voiding transactions
Applying discounts above cashier limits
Accessing sensitive reports
Modifying system settings
Next steps
Roles and permissions Learn about role-based access control
Audit logging Track user activity and system events