Authenticator Login
Overviewโ
Login using Time-based One-Time Password (TOTP) codes from authenticator apps. This guide assumes authenticator setup is already complete.
Prerequisitesโ
- User has completed authenticator setup
- User has authenticator app installed and configured
- Secret key is stored in user's account
Login Flowโ
Step 1: Initial Login Requestโ
POST /api/BPMSelfService/commands/SelfAdminLoginCommand
{
"username": "admin@example.com",
"password": "SecurePassword123!",
"verificationMethodType": 3
}
Step 2: Server Responseโ
Since authenticator is already set up:
{
"status": "success",
"message": "Please enter authenticator code",
"data": {
"requiresSetup": false,
"requiresOtp": true
}
}
Step 3: User Opens Authenticator Appโ
User opens their authenticator app and finds the BankLingo entry, which displays a 6-digit code that refreshes every 30 seconds.
Step 4: Validate TOTP Codeโ
POST /api/BPMSelfService/commands/ValidateOtpCommand
{
"userId": 123,
"otpCode": "123456",
"verificationMethodType": 3
}
Step 5: Successful Authenticationโ
{
"status": "success",
"message": "Login successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "...",
"expiresIn": 3600,
"user": {
"id": 123,
"username": "admin",
"email": "admin@example.com"
}
}
}
Complete Frontend Exampleโ
React Exampleโ
function AuthenticatorLogin() {
const [step, setStep] = useState('credentials'); // 'credentials' | 'totp'
const [credentials, setCredentials] = useState({ username: '', password: '' });
const [totpCode, setTotpCode] = useState('');
const [error, setError] = useState('');
const [userId, setUserId] = useState(null);
// Step 1: Submit credentials
const handleCredentialsSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/BPMSelfService/commands/SelfAdminLoginCommand', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...credentials,
verificationMethodType: 3
})
});
const result = await response.json();
if (result.data.requiresSetup) {
// Redirect to setup flow
window.location.href = '/authenticator-setup';
} else if (result.data.requiresOtp) {
setUserId(result.data.userId);
setStep('totp');
}
} catch (err) {
setError('Login failed. Please try again.');
}
};
// Step 2: Validate TOTP code
const handleTotpSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/BPMSelfService/commands/ValidateOtpCommand', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: userId,
otpCode: totpCode,
verificationMethodType: 3
})
});
const result = await response.json();
if (result.status === 'success') {
// Store token and redirect
localStorage.setItem('jwt_token', result.data.token);
window.location.href = '/dashboard';
} else {
setError('Invalid code. Please try again.');
setTotpCode('');
}
} catch (err) {
setError('Validation failed. Please try again.');
}
};
if (step === 'credentials') {
return (
<form onSubmit={handleCredentialsSubmit}>
<h2>Admin Login</h2>
<input
type="text"
placeholder="Username"
value={credentials.username}
onChange={(e) => setCredentials({...credentials, username: e.target.value})}
/>
<input
type="password"
placeholder="Password"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
/>
<button type="submit">Continue</button>
{error && <div className="error">{error}</div>}
</form>
);
}
return (
<form onSubmit={handleTotpSubmit}>
<h2>Enter Authenticator Code</h2>
<p>Open your authenticator app and enter the 6-digit code for BankLingo</p>
<input
type="text"
placeholder="000000"
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ''))}
maxLength={6}
autoFocus
/>
<button type="submit" disabled={totpCode.length !== 6}>
Verify
</button>
{error && <div className="error">{error}</div>}
<a href="#" onClick={() => setStep('credentials')}>รขโ ย Back to login</a>
</form>
);
}
C# Desktop Exampleโ
Code Removed
Implementation details removed for security.
Contact support for implementation guidance.
Code Validationโ
Time Windowโ
- TOTP codes are valid for 30 seconds
- System accepts codes within รยฑ30 second window
- Total acceptance window: 90 seconds
Example Timelineโ
Time: 00:00 00:30 01:00 01:30
Code: 123456 789012 456789 ...
[-------- Valid --------]
[-30s to +30s window]
Error Handlingโ
Invalid Codeโ
{
"status": "error",
"message": "Invalid authenticator code",
"errorCode": "AUTH_002"
}
Common causes:
- User entered wrong code
- Code expired (past 30-second window)
- Time synchronization issue
Account Lockedโ
{
"status": "error",
"message": "Account locked due to multiple failed attempts",
"errorCode": "AUTH_004",
"lockedUntil": "2025-01-15T14:30:00Z"
}
After 5 failed attempts, account is temporarily locked (15 minutes).
Best Practicesโ
User Experienceโ
- Auto-submit on 6 digits: Submit form automatically when 6 digits entered
- Show countdown: Display time remaining for current code
- Clear on failure: Clear input field after failed attempt
- Helpful errors: Show specific error messages
- Back button: Allow user to return to credentials step
Securityโ
- Rate limiting: Limit validation attempts (5 per 15 minutes)
- Account lockout: Temporary lockout after failed attempts
- Audit logging: Log all authentication attempts
- HTTPS only: Never transmit codes over HTTP
- Token security: Store JWT securely (HttpOnly cookies)
Time Synchronizationโ
- Server and user device clocks must be reasonably synchronized
- System tolerates รยฑ30 seconds drift
- Recommend enabling automatic time sync on devices
Troubleshootingโ
| Problem | Solution |
|---|---|
| Code always rejected | Check device time synchronization |
| "Account locked" error | Wait 15 minutes or contact admin |
| Code expired message | Enter new code immediately after it appears |
| Lost authenticator | Contact admin for account recovery |