Commit f206e473 by Michael Brachmann

simple key device registration option as an alternative to webauthn

parent 89533881
......@@ -88,6 +88,14 @@ type
function LoginAutomatic(const CredentialId, ChallengeToken,
AuthenticatorData, ClientDataJSON,
Signature: string): string;
// Simple-key registration — client generates key, no crypto verification
function CompleteRegistrationSimple(const PhoneNumber, DeviceKey,
ChallengeToken: string): TJSONObject;
// Simple-key auto-login — verifies challenge token + credential ownership
function LoginDeviceKey(const CredentialId, ChallengeToken: string): string;
// Simple-key password login — password + challenge token (first login / fallback)
function LoginPasswordAndSimpleKey(const User, Password, Agency,
CredentialId, ChallengeToken: string): string;
end;
implementation
......
......@@ -48,6 +48,11 @@ type
function LoginAutomatic(const CredentialId, ChallengeToken,
AuthenticatorData, ClientDataJSON,
Signature: string): string;
function CompleteRegistrationSimple(const PhoneNumber, DeviceKey,
ChallengeToken: string): TJSONObject;
function LoginDeviceKey(const CredentialId, ChallengeToken: string): string;
function LoginPasswordAndSimpleKey(const User, Password, Agency,
CredentialId, ChallengeToken: string): string;
end;
implementation
......@@ -965,6 +970,237 @@ begin
end;
// ---------------------------------------------------------------------------
// CompleteRegistrationSimple — client-generated key, no crypto verification
// ---------------------------------------------------------------------------
function TAuthService.CompleteRegistrationSimple(const PhoneNumber, DeviceKey,
ChallengeToken: string): TJSONObject;
var
challengeB64, normalizedPhone: string;
q: TUniQuery;
begin
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
Logger.Log(2, 'AuthService.CompleteRegistrationSimple - phone: "' + PhoneNumber + '"');
if not VerifyChallengeToken(ChallengeToken, 'reg', challengeB64) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired registration challenge.');
Exit;
end;
if Trim(DeviceKey) = '' then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Device key is required.');
Exit;
end;
try
normalizedPhone := NormalizePhoneE164(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Exit;
end;
end;
q := TUniQuery.Create(nil);
try
q.Connection := authDB.ucLemsOCSO;
var ctx := THttpServerContext.Current;
var userAgent: string := '';
if ctx <> nil then
userAgent := ctx.Request.Headers.Get('User-Agent');
q.SQL.Text :=
'UPDATE lems.device_registrations ' +
'SET credential_id = :CID, user_agent = :AGENT, ' +
' key_type = ''simple'', ' +
' status = ''active'', registered_at = NOW() ' +
'WHERE phone_number = :PHONE AND status = ''pending''';
q.ParamByName('CID').AsString := Trim(DeviceKey);
q.ParamByName('AGENT').AsString := userAgent;
q.ParamByName('PHONE').AsString := normalizedPhone;
q.ExecSQL;
if q.RowsAffected = 0 then
begin
Logger.Log(2, 'CompleteRegistrationSimple - no pending row for "' + normalizedPhone + '"');
Result.AddPair('status', 'error');
Result.AddPair('message', 'Phone number is not pending registration. Contact your administrator.');
Exit;
end;
finally
q.Free;
end;
Logger.Log(2, 'CompleteRegistrationSimple - activated simple key for "' + normalizedPhone + '"');
Result.AddPair('status', 'ok');
Result.AddPair('message', 'Device registered successfully.');
Result.AddPair('credentialId', Trim(DeviceKey));
end;
// ---------------------------------------------------------------------------
// LoginDeviceKey — auto-login for simple-key devices (no password)
// ---------------------------------------------------------------------------
function TAuthService.LoginDeviceKey(const CredentialId, ChallengeToken: string): string;
var
challengeB64: string;
storedUsername, storedAgency: string;
q: TUniQuery;
JWT: TJWT;
begin
Logger.Log(2, 'AuthService.LoginDeviceKey - credId: ' + Copy(CredentialId, 1, 20));
if not VerifyChallengeToken(ChallengeToken, 'auth', challengeB64) then
raise EXDataHttpUnauthorized.Create('Invalid or expired authentication challenge.');
storedUsername := '';
storedAgency := '';
q := TUniQuery.Create(nil);
try
q.Connection := authDB.ucLemsOCSO;
q.SQL.Text :=
'SELECT username, agency FROM lems.device_registrations ' +
'WHERE credential_id = :CID AND key_type = ''simple'' AND status = ''active''';
q.ParamByName('CID').AsString := Trim(CredentialId);
q.Open;
try
if q.IsEmpty then
raise EXDataHttpUnauthorized.Create('Device not registered, revoked, or not a simple-key device.');
storedUsername := q.FieldByName('username').AsString;
storedAgency := q.FieldByName('agency').AsString;
finally
q.Close;
end;
finally
q.Free;
end;
if (storedUsername = '') or (storedAgency = '') then
raise EXDataHttpUnauthorized.Create(
'No username saved for this device. Please sign in with your username and password first.');
if not LoadUserByName(storedUsername, storedAgency) then
raise EXDataHttpUnauthorized.Create(
Format('User "%s" not found or inactive.', [storedUsername]));
Logger.Log(2, Format('AuthService.LoginDeviceKey - success for User: "%s"', [userName]));
JWT := TJWT.Create;
try
JWT.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
JWT.Claims.IssuedAt := Now;
JWT.Claims.Expiration := IncHour(Now, 24);
JWT.Claims.SetClaimOfType<string>('user_name', userName);
JWT.Claims.SetClaimOfType<string>('user_fullname', userFullName);
JWT.Claims.SetClaimOfType<string>('user_agency', userAgency);
JWT.Claims.SetClaimOfType<string>('user_badge', userBadge);
JWT.Claims.SetClaimOfType<string>('user_id', userId);
JWT.Claims.SetClaimOfType<string>('user_personnelid', userPersonnelId);
JWT.Claims.SetClaimOfType<Boolean>('user_admin', userIsAdmin);
JWT.Claims.SetClaimOfType<string>('credential_id', Trim(CredentialId));
Result := TJOSE.SHA256CompactToken(ServerConfig.jwtTokenSecret, JWT);
finally
JWT.Free;
end;
end;
// ---------------------------------------------------------------------------
// LoginPasswordAndSimpleKey — password login for simple-key devices
// ---------------------------------------------------------------------------
function TAuthService.LoginPasswordAndSimpleKey(const User, Password, Agency,
CredentialId, ChallengeToken: string): string;
var
challengeB64: string;
userState: Integer;
q: TUniQuery;
JWT: TJWT;
begin
Logger.Log(1, Format('AuthService.LoginPasswordAndSimpleKey - User: "%s" Agency: "%s"', [User, Agency]));
try
userState := CheckUser(User, Password, Agency);
except
on E: Exception do
begin
Logger.Log(2, 'LoginPasswordAndSimpleKey - CheckUser error: ' + E.ClassName + ': ' + E.Message);
raise EXDataHttpException.Create(500, 'Login failed');
end;
end;
if userState = 0 then
raise EXDataHttpUnauthorized.Create('Invalid user or password');
if userState = 1 then
raise EXDataHttpUnauthorized.Create('User not active');
if not VerifyChallengeToken(ChallengeToken, 'auth', challengeB64) then
raise EXDataHttpUnauthorized.Create('Invalid or expired authentication challenge.');
q := TUniQuery.Create(nil);
try
q.Connection := authDB.ucLemsOCSO;
q.SQL.Text :=
'SELECT id FROM lems.device_registrations ' +
'WHERE credential_id = :CID AND key_type = ''simple'' AND status = ''active''';
q.ParamByName('CID').AsString := Trim(CredentialId);
q.Open;
try
if q.IsEmpty then
raise EXDataHttpUnauthorized.Create('Device not registered or not a simple-key device.');
finally
q.Close;
end;
finally
q.Free;
end;
Logger.Log(2, Format('AuthService.LoginPasswordAndSimpleKey - success for User: "%s"', [User]));
JWT := TJWT.Create;
try
JWT.Claims.JWTId := LowerCase(Copy(TUtils.GuidToVariant(TUtils.NewGuid), 2, 36));
JWT.Claims.IssuedAt := Now;
JWT.Claims.Expiration := IncHour(Now, 24);
JWT.Claims.SetClaimOfType<string>('user_name', userName);
JWT.Claims.SetClaimOfType<string>('user_fullname', userFullName);
JWT.Claims.SetClaimOfType<string>('user_agency', userAgency);
JWT.Claims.SetClaimOfType<string>('user_badge', userBadge);
JWT.Claims.SetClaimOfType<string>('user_id', userId);
JWT.Claims.SetClaimOfType<string>('user_personnelid', userPersonnelId);
JWT.Claims.SetClaimOfType<Boolean>('user_admin', userIsAdmin);
JWT.Claims.SetClaimOfType<string>('credential_id', Trim(CredentialId));
Result := TJOSE.SHA256CompactToken(ServerConfig.jwtTokenSecret, JWT);
finally
JWT.Free;
end;
// Persist username + agency so future auto-logins work
q := TUniQuery.Create(nil);
try
q.Connection := authDB.ucLemsOCSO;
q.SQL.Text :=
'UPDATE lems.device_registrations ' +
'SET username = :UNAME, agency = :AGCY ' +
'WHERE credential_id = :CID';
q.ParamByName('UNAME').AsString := userName;
q.ParamByName('AGCY').AsString := userAgency;
q.ParamByName('CID').AsString := Trim(CredentialId);
q.ExecSQL;
finally
q.Free;
end;
end;
// ---------------------------------------------------------------------------
// Existing methods (unchanged)
// ---------------------------------------------------------------------------
......
-- Add key_type column to distinguish WebAuthn vs simple-key devices
ALTER TABLE lems.device_registrations
ADD COLUMN IF NOT EXISTS key_type VARCHAR(10) DEFAULT 'webauthn';
-- Back-fill existing active rows as webauthn
UPDATE lems.device_registrations
SET key_type = 'webauthn'
WHERE key_type IS NULL AND status = 'active';
......@@ -9,6 +9,7 @@ uses
const
TOKEN_NAME = 'WEBEMIMOBILE_TOKEN';
CREDENTIAL_NAME = 'WEBEMIMOBILE_CREDENTIAL_ID';
KEY_TYPE_NAME = 'WEBEMIMOBILE_KEY_TYPE';
type
TOnLoginSuccess = reference to procedure;
......@@ -25,6 +26,7 @@ type
procedure SetToken(AToken: string);
procedure DeleteToken;
procedure SetCredentialId(AId: string);
procedure SetKeyType(AType: string);
public
constructor Create; reintroduce;
destructor Destroy; override;
......@@ -37,9 +39,10 @@ type
function TokenExpired: Boolean;
function TokenPayload: JS.TJSObject;
// Credential (WebAuthn) storage
// Credential storage
function GetCredentialId: string;
function IsDeviceRegistered: Boolean;
function IsSimpleKey: Boolean;
procedure ClearCredentialId;
// WebAuthn registration — two-step
......@@ -60,6 +63,15 @@ type
procedure LoginAutomatic(ACredentialId, AChallengeToken,
AAuthenticatorData, AClientDataJSON, ASignature: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
// Simple-key registration and login
procedure CompleteRegistrationSimple(APhoneNumber, ADeviceKey, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
procedure LoginDeviceKey(ACredentialId, AChallengeToken: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure LoginPasswordAndSimpleKey(AUser, APassword, AAgency,
ACredentialId, AChallengeToken: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
end;
TJwtHelper = class
......@@ -137,9 +149,20 @@ begin
window.localStorage.setItem(CREDENTIAL_NAME, AId);
end;
procedure TAuthService.SetKeyType(AType: string);
begin
window.localStorage.setItem(KEY_TYPE_NAME, AType);
end;
procedure TAuthService.ClearCredentialId;
begin
window.localStorage.removeItem(CREDENTIAL_NAME);
window.localStorage.removeItem(KEY_TYPE_NAME);
end;
function TAuthService.IsSimpleKey: Boolean;
begin
Result := window.localStorage.getItem(KEY_TYPE_NAME) = 'simple';
end;
function TAuthService.GetCredentialId: string;
......@@ -207,6 +230,7 @@ procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId,
else if status = 'ok' then
begin
SetCredentialId(credId);
SetKeyType('webauthn');
ASuccess;
end
else
......@@ -333,6 +357,95 @@ begin
);
end;
// ---- Simple-key registration and login ----
procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceKey, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
var
resp: JS.TJSObject;
status, msg, credId: string;
begin
resp := JS.TJSObject(Response.Result);
status := JS.toString(resp.Properties['status']);
msg := JS.toString(resp.Properties['message']);
credId := JS.toString(resp.Properties['credentialId']);
if status = 'ok' then
begin
SetCredentialId(credId);
SetKeyType('simple');
ASuccess;
end
else
AError(msg);
end;
procedure OnError(Error: TXDataClientError);
begin
AError(Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
end;
begin
FClient.RawInvoke(
'IAuthService.CompleteRegistrationSimple',
[APhoneNumber, ADeviceKey, AChallengeToken],
@OnLoad, @OnError
);
end;
procedure TAuthService.LoginDeviceKey(ACredentialId, AChallengeToken: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse);
var
Token: JS.TJSObject;
begin
Token := JS.TJSObject(Response.Result);
SetToken(JS.toString(Token.Properties['value']));
ASuccess;
end;
procedure OnError(Error: TXDataClientError);
begin
AError(Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
end;
begin
FClient.RawInvoke(
'IAuthService.LoginDeviceKey',
[ACredentialId, AChallengeToken],
@OnLoad, @OnError
);
end;
procedure TAuthService.LoginPasswordAndSimpleKey(AUser, APassword, AAgency,
ACredentialId, AChallengeToken: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
procedure OnLoad(Response: TXDataClientResponse);
var
Token: JS.TJSObject;
begin
Token := JS.TJSObject(Response.Result);
SetToken(JS.toString(Token.Properties['value']));
ASuccess;
end;
procedure OnError(Error: TXDataClientError);
begin
AError(Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
end;
begin
FClient.RawInvoke(
'IAuthService.LoginPasswordAndSimpleKey',
[AUser, APassword, AAgency, ACredentialId, AChallengeToken],
@OnLoad, @OnError
);
end;
// ---- Token helpers ----
function TAuthService.TokenExpirationDate: TDateTime;
......
......@@ -18,6 +18,19 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
TextHint = '(303) 555-1234'
WidthPercent = 100.000000000000000000
end
object chkUseWebAuthn: TWebCheckBox
Left = 240
Top = 163
Width = 160
Height = 21
Caption = 'Use Passkey (WebAuthn)'
Checked = True
ElementID = 'view.devicereg.chkwebauthn'
HeightPercent = 100.000000000000000000
State = cbChecked
TabOrder = 1
WidthPercent = 100.000000000000000000
end
object btnRegister: TWebButton
Left = 240
Top = 190
......@@ -26,7 +39,7 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
Caption = 'Register This Device'
ElementID = 'view.devicereg.btnregister'
HeightPercent = 100.000000000000000000
TabOrder = 1
TabOrder = 2
WidthPercent = 100.000000000000000000
OnClick = btnRegisterClick
end
......
......@@ -31,12 +31,18 @@
aria-label="Close"></button>
</div>
<p class="text-muted small mb-3">
<p id="view.devicereg.desc-webauthn" class="text-muted small mb-3">
This browser has not been registered for emiMobile access.
Enter the phone number your administrator registered for this device,
then click <strong>Register</strong>.
Your browser will prompt you to verify with a PIN, fingerprint, or security key.
</p>
<p id="view.devicereg.desc-simplekey" class="text-muted small mb-3 d-none">
This browser has not been registered for emiMobile access.
Enter the phone number your administrator registered for this device,
then click <strong>Register</strong>.
A secure key will be generated and stored in your browser.
</p>
<div class="mb-3">
<label class="form-label small text-muted">Phone number</label>
......@@ -54,6 +60,25 @@
style="max-width:100%; overflow:hidden; white-space:nowrap;"></p>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="view.devicereg.chkwebauthn" checked>
<label class="form-check-label small text-muted"
for="view.devicereg.chkwebauthn">
Use Passkey (WebAuthn)
</label>
</div>
<p id="view.devicereg.chk-webauthn-help"
class="text-muted mb-0" style="font-size:0.75rem; padding-left:1.5rem;">
Biometrics, PIN, or security key — hardware-backed.
</p>
<p id="view.devicereg.chk-simplekey-help"
class="text-muted mb-0 d-none" style="font-size:0.75rem; padding-left:1.5rem;">
Browser-stored key — no hardware required.
</p>
</div>
<button id="view.devicereg.btnregister"
class="btn btn-primary w-100">
Register This Device
......
......@@ -11,6 +11,7 @@ uses
type
TFViewDeviceRegistration = class(TWebForm)
edtPhoneNumber: TWebEdit;
chkUseWebAuthn: TWebCheckBox;
btnRegister: TWebButton;
pnlMessage: TWebPanel;
lblMessage: TWebLabel;
......@@ -25,6 +26,7 @@ type
procedure HideNotification;
procedure SetBusy(ABusy: Boolean);
procedure DoWebAuthnCreate(APhoneNumber, AChallenge, AChallengeToken: string);
procedure DoSimpleKeyCreate(APhoneNumber, AChallengeToken: string);
public
class procedure Display(ARegistrationProc: TSuccessProc);
end;
......@@ -77,6 +79,28 @@ begin
inp.value = formatted;
});
}
// Toggle description text when checkbox changes
var chk = document.getElementById('view.devicereg.chkwebauthn');
if (chk) {
chk.addEventListener('change', function() {
var waDesc = document.getElementById('view.devicereg.desc-webauthn');
var skDesc = document.getElementById('view.devicereg.desc-simplekey');
var waHelp = document.getElementById('view.devicereg.chk-webauthn-help');
var skHelp = document.getElementById('view.devicereg.chk-simplekey-help');
if (chk.checked) {
if (waDesc) waDesc.classList.remove('d-none');
if (skDesc) skDesc.classList.add('d-none');
if (waHelp) waHelp.classList.remove('d-none');
if (skHelp) skHelp.classList.add('d-none');
} else {
if (waDesc) waDesc.classList.add('d-none');
if (skDesc) skDesc.classList.remove('d-none');
if (waHelp) waHelp.classList.add('d-none');
if (skHelp) skHelp.classList.remove('d-none');
}
});
}
end;
end;
......@@ -84,9 +108,14 @@ procedure TFViewDeviceRegistration.SetBusy(ABusy: Boolean);
begin
asm
var btn = document.getElementById('view.devicereg.btnregister');
var chk = document.getElementById('view.devicereg.chkwebauthn');
if (btn) {
btn.disabled = ABusy;
btn.textContent = ABusy ? 'Waiting for authenticator...' : 'Register This Device';
if (ABusy) {
btn.textContent = (chk && chk.checked) ? 'Waiting for authenticator...' : 'Activating device...';
} else {
btn.textContent = 'Register This Device';
}
}
end;
end;
......@@ -94,10 +123,14 @@ end;
procedure TFViewDeviceRegistration.btnRegisterClick(Sender: TObject);
var
phoneNumber: string;
useWebAuthn: Boolean;
procedure OnBeginOK(AChallenge, AChallengeToken: string);
begin
DoWebAuthnCreate(phoneNumber, AChallenge, AChallengeToken);
if useWebAuthn then
DoWebAuthnCreate(phoneNumber, AChallenge, AChallengeToken)
else
DoSimpleKeyCreate(phoneNumber, AChallengeToken);
end;
procedure OnBeginError(AMsg: string);
......@@ -114,6 +147,7 @@ begin
Exit;
end;
useWebAuthn := chkUseWebAuthn.Checked;
SetBusy(True);
HideNotification;
......@@ -198,6 +232,46 @@ begin
end;
end;
procedure TFViewDeviceRegistration.DoSimpleKeyCreate(APhoneNumber, AChallengeToken: string);
var
phoneNumber, challengeToken, deviceKey: string;
procedure OnCompleteOK;
begin
FRegistrationProc;
end;
procedure OnCompleteError(AMsg: string);
begin
SetBusy(False);
ShowNotification(AMsg);
end;
begin
phoneNumber := APhoneNumber;
challengeToken := AChallengeToken;
deviceKey := '';
asm
var keyBytes = new Uint8Array(32);
crypto.getRandomValues(keyBytes);
var bin = String.fromCharCode.apply(null, keyBytes);
deviceKey = btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
end;
if deviceKey = '' then
begin
SetBusy(False);
ShowNotification('Failed to generate device key.');
Exit;
end;
AuthService.CompleteRegistrationSimple(
phoneNumber, deviceKey, challengeToken,
@OnCompleteOK, @OnCompleteError
);
end;
procedure TFViewDeviceRegistration.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
......
......@@ -79,6 +79,39 @@ var
credId: string;
procedure OnDeviceUser(AUsername, AAgency: string);
procedure OnAutoLoginOK;
begin
FLoginProc;
end;
procedure OnAutoLoginError(AMsg: string);
begin
asm
var autoSection = document.getElementById('view.login.autosection');
var manualSection = document.getElementById('view.login.manualsection');
if (autoSection) autoSection.classList.add('d-none');
if (manualSection) manualSection.classList.remove('d-none');
end;
ShowNotification(AMsg);
end;
procedure OnBeginForSimpleOK(AChallenge, AChallengeToken: string);
begin
AuthService.LoginDeviceKey(credId, AChallengeToken, @OnAutoLoginOK, @OnAutoLoginError);
end;
procedure OnBeginForSimpleError(AMsg: string);
begin
asm
var autoSection = document.getElementById('view.login.autosection');
var manualSection = document.getElementById('view.login.manualsection');
if (autoSection) autoSection.classList.add('d-none');
if (manualSection) manualSection.classList.remove('d-none');
end;
ShowNotification('Login Error: ' + AMsg);
end;
begin
FAutoUsername := AUsername;
FAutoAgency := AAgency;
......@@ -102,6 +135,16 @@ var
});
}
end;
if AuthService.IsSimpleKey then
begin
// Hide passkey button — simple-key auto-login needs no hardware interaction
asm
var btn = document.getElementById('view.login.btnpasskeylogin');
if (btn) btn.style.display = 'none';
end;
AuthService.BeginAuthentication(credId, @OnBeginForSimpleOK, @OnBeginForSimpleError);
end;
end;
end;
......@@ -135,8 +178,24 @@ procedure TFViewLogin.btnLoginClick(Sender: TObject);
var
user, password, agency, credentialId: string;
procedure OnLoginOK;
begin
FLoginProc;
end;
procedure OnLoginError(AMsg: string);
begin
SetBusy(False);
ShowNotification('Login Error: ' + AMsg);
end;
procedure OnBeginOK(AChallenge, AChallengeToken: string);
begin
if AuthService.IsSimpleKey then
AuthService.LoginPasswordAndSimpleKey(
user, password, agency, credentialId, AChallengeToken,
@OnLoginOK, @OnLoginError)
else
DoWebAuthnGet(user, password, agency, credentialId, AChallenge, AChallengeToken);
end;
......@@ -174,8 +233,28 @@ procedure TFViewLogin.btnPasskeyLoginClick(Sender: TObject);
var
credId: string;
procedure OnLoginOK;
begin
FLoginProc;
end;
procedure OnLoginError(AMsg: string);
begin
SetBusy(False);
ShowNotification('Login Error: ' + AMsg);
asm
var autoSection = document.getElementById('view.login.autosection');
var manualSection = document.getElementById('view.login.manualsection');
if (autoSection) autoSection.classList.add('d-none');
if (manualSection) manualSection.classList.remove('d-none');
end;
end;
procedure OnBeginOK(AChallenge, AChallengeToken: string);
begin
if AuthService.IsSimpleKey then
AuthService.LoginDeviceKey(credId, AChallengeToken, @OnLoginOK, @OnLoginError)
else
DoPasskeyAutoLogin(credId, AChallenge, AChallengeToken);
end;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment