Commit f206e473 by Michael Brachmann

simple key device registration option as an alternative to webauthn

parent 89533881
...@@ -88,6 +88,14 @@ type ...@@ -88,6 +88,14 @@ type
function LoginAutomatic(const CredentialId, ChallengeToken, function LoginAutomatic(const CredentialId, ChallengeToken,
AuthenticatorData, ClientDataJSON, AuthenticatorData, ClientDataJSON,
Signature: string): string; 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; end;
implementation implementation
......
...@@ -48,6 +48,11 @@ type ...@@ -48,6 +48,11 @@ type
function LoginAutomatic(const CredentialId, ChallengeToken, function LoginAutomatic(const CredentialId, ChallengeToken,
AuthenticatorData, ClientDataJSON, AuthenticatorData, ClientDataJSON,
Signature: string): string; 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; end;
implementation implementation
...@@ -965,6 +970,237 @@ begin ...@@ -965,6 +970,237 @@ begin
end; 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) // 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 ...@@ -9,6 +9,7 @@ uses
const const
TOKEN_NAME = 'WEBEMIMOBILE_TOKEN'; TOKEN_NAME = 'WEBEMIMOBILE_TOKEN';
CREDENTIAL_NAME = 'WEBEMIMOBILE_CREDENTIAL_ID'; CREDENTIAL_NAME = 'WEBEMIMOBILE_CREDENTIAL_ID';
KEY_TYPE_NAME = 'WEBEMIMOBILE_KEY_TYPE';
type type
TOnLoginSuccess = reference to procedure; TOnLoginSuccess = reference to procedure;
...@@ -25,6 +26,7 @@ type ...@@ -25,6 +26,7 @@ type
procedure SetToken(AToken: string); procedure SetToken(AToken: string);
procedure DeleteToken; procedure DeleteToken;
procedure SetCredentialId(AId: string); procedure SetCredentialId(AId: string);
procedure SetKeyType(AType: string);
public public
constructor Create; reintroduce; constructor Create; reintroduce;
destructor Destroy; override; destructor Destroy; override;
...@@ -37,9 +39,10 @@ type ...@@ -37,9 +39,10 @@ type
function TokenExpired: Boolean; function TokenExpired: Boolean;
function TokenPayload: JS.TJSObject; function TokenPayload: JS.TJSObject;
// Credential (WebAuthn) storage // Credential storage
function GetCredentialId: string; function GetCredentialId: string;
function IsDeviceRegistered: Boolean; function IsDeviceRegistered: Boolean;
function IsSimpleKey: Boolean;
procedure ClearCredentialId; procedure ClearCredentialId;
// WebAuthn registration — two-step // WebAuthn registration — two-step
...@@ -60,6 +63,15 @@ type ...@@ -60,6 +63,15 @@ type
procedure LoginAutomatic(ACredentialId, AChallengeToken, procedure LoginAutomatic(ACredentialId, AChallengeToken,
AAuthenticatorData, AClientDataJSON, ASignature: string; AAuthenticatorData, AClientDataJSON, ASignature: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError); 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; end;
TJwtHelper = class TJwtHelper = class
...@@ -137,9 +149,20 @@ begin ...@@ -137,9 +149,20 @@ begin
window.localStorage.setItem(CREDENTIAL_NAME, AId); window.localStorage.setItem(CREDENTIAL_NAME, AId);
end; end;
procedure TAuthService.SetKeyType(AType: string);
begin
window.localStorage.setItem(KEY_TYPE_NAME, AType);
end;
procedure TAuthService.ClearCredentialId; procedure TAuthService.ClearCredentialId;
begin begin
window.localStorage.removeItem(CREDENTIAL_NAME); 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; end;
function TAuthService.GetCredentialId: string; function TAuthService.GetCredentialId: string;
...@@ -207,6 +230,7 @@ procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId, ...@@ -207,6 +230,7 @@ procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId,
else if status = 'ok' then else if status = 'ok' then
begin begin
SetCredentialId(credId); SetCredentialId(credId);
SetKeyType('webauthn');
ASuccess; ASuccess;
end end
else else
...@@ -333,6 +357,95 @@ begin ...@@ -333,6 +357,95 @@ begin
); );
end; 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 ---- // ---- Token helpers ----
function TAuthService.TokenExpirationDate: TDateTime; function TAuthService.TokenExpirationDate: TDateTime;
......
...@@ -18,6 +18,19 @@ object FViewDeviceRegistration: TFViewDeviceRegistration ...@@ -18,6 +18,19 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
TextHint = '(303) 555-1234' TextHint = '(303) 555-1234'
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
end 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 object btnRegister: TWebButton
Left = 240 Left = 240
Top = 190 Top = 190
...@@ -26,7 +39,7 @@ object FViewDeviceRegistration: TFViewDeviceRegistration ...@@ -26,7 +39,7 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
Caption = 'Register This Device' Caption = 'Register This Device'
ElementID = 'view.devicereg.btnregister' ElementID = 'view.devicereg.btnregister'
HeightPercent = 100.000000000000000000 HeightPercent = 100.000000000000000000
TabOrder = 1 TabOrder = 2
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnRegisterClick OnClick = btnRegisterClick
end end
......
...@@ -31,12 +31,18 @@ ...@@ -31,12 +31,18 @@
aria-label="Close"></button> aria-label="Close"></button>
</div> </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. This browser has not been registered for emiMobile access.
Enter the phone number your administrator registered for this device, Enter the phone number your administrator registered for this device,
then click <strong>Register</strong>. then click <strong>Register</strong>.
Your browser will prompt you to verify with a PIN, fingerprint, or security key. Your browser will prompt you to verify with a PIN, fingerprint, or security key.
</p> </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"> <div class="mb-3">
<label class="form-label small text-muted">Phone number</label> <label class="form-label small text-muted">Phone number</label>
...@@ -54,6 +60,25 @@ ...@@ -54,6 +60,25 @@
style="max-width:100%; overflow:hidden; white-space:nowrap;"></p> style="max-width:100%; overflow:hidden; white-space:nowrap;"></p>
</div> </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" <button id="view.devicereg.btnregister"
class="btn btn-primary w-100"> class="btn btn-primary w-100">
Register This Device Register This Device
......
...@@ -11,6 +11,7 @@ uses ...@@ -11,6 +11,7 @@ uses
type type
TFViewDeviceRegistration = class(TWebForm) TFViewDeviceRegistration = class(TWebForm)
edtPhoneNumber: TWebEdit; edtPhoneNumber: TWebEdit;
chkUseWebAuthn: TWebCheckBox;
btnRegister: TWebButton; btnRegister: TWebButton;
pnlMessage: TWebPanel; pnlMessage: TWebPanel;
lblMessage: TWebLabel; lblMessage: TWebLabel;
...@@ -25,6 +26,7 @@ type ...@@ -25,6 +26,7 @@ type
procedure HideNotification; procedure HideNotification;
procedure SetBusy(ABusy: Boolean); procedure SetBusy(ABusy: Boolean);
procedure DoWebAuthnCreate(APhoneNumber, AChallenge, AChallengeToken: string); procedure DoWebAuthnCreate(APhoneNumber, AChallenge, AChallengeToken: string);
procedure DoSimpleKeyCreate(APhoneNumber, AChallengeToken: string);
public public
class procedure Display(ARegistrationProc: TSuccessProc); class procedure Display(ARegistrationProc: TSuccessProc);
end; end;
...@@ -77,6 +79,28 @@ begin ...@@ -77,6 +79,28 @@ begin
inp.value = formatted; 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;
end; end;
...@@ -84,9 +108,14 @@ procedure TFViewDeviceRegistration.SetBusy(ABusy: Boolean); ...@@ -84,9 +108,14 @@ procedure TFViewDeviceRegistration.SetBusy(ABusy: Boolean);
begin begin
asm asm
var btn = document.getElementById('view.devicereg.btnregister'); var btn = document.getElementById('view.devicereg.btnregister');
var chk = document.getElementById('view.devicereg.chkwebauthn');
if (btn) { if (btn) {
btn.disabled = ABusy; 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;
end; end;
...@@ -94,10 +123,14 @@ end; ...@@ -94,10 +123,14 @@ end;
procedure TFViewDeviceRegistration.btnRegisterClick(Sender: TObject); procedure TFViewDeviceRegistration.btnRegisterClick(Sender: TObject);
var var
phoneNumber: string; phoneNumber: string;
useWebAuthn: Boolean;
procedure OnBeginOK(AChallenge, AChallengeToken: string); procedure OnBeginOK(AChallenge, AChallengeToken: string);
begin begin
DoWebAuthnCreate(phoneNumber, AChallenge, AChallengeToken); if useWebAuthn then
DoWebAuthnCreate(phoneNumber, AChallenge, AChallengeToken)
else
DoSimpleKeyCreate(phoneNumber, AChallengeToken);
end; end;
procedure OnBeginError(AMsg: string); procedure OnBeginError(AMsg: string);
...@@ -114,6 +147,7 @@ begin ...@@ -114,6 +147,7 @@ begin
Exit; Exit;
end; end;
useWebAuthn := chkUseWebAuthn.Checked;
SetBusy(True); SetBusy(True);
HideNotification; HideNotification;
...@@ -198,6 +232,46 @@ begin ...@@ -198,6 +232,46 @@ begin
end; end;
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); procedure TFViewDeviceRegistration.btnCloseNotificationClick(Sender: TObject);
begin begin
HideNotification; HideNotification;
......
...@@ -79,6 +79,39 @@ var ...@@ -79,6 +79,39 @@ var
credId: string; credId: string;
procedure OnDeviceUser(AUsername, AAgency: 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 begin
FAutoUsername := AUsername; FAutoUsername := AUsername;
FAutoAgency := AAgency; FAutoAgency := AAgency;
...@@ -102,6 +135,16 @@ var ...@@ -102,6 +135,16 @@ var
}); });
} }
end; 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;
end; end;
...@@ -135,8 +178,24 @@ procedure TFViewLogin.btnLoginClick(Sender: TObject); ...@@ -135,8 +178,24 @@ procedure TFViewLogin.btnLoginClick(Sender: TObject);
var var
user, password, agency, credentialId: string; 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); procedure OnBeginOK(AChallenge, AChallengeToken: string);
begin begin
if AuthService.IsSimpleKey then
AuthService.LoginPasswordAndSimpleKey(
user, password, agency, credentialId, AChallengeToken,
@OnLoginOK, @OnLoginError)
else
DoWebAuthnGet(user, password, agency, credentialId, AChallenge, AChallengeToken); DoWebAuthnGet(user, password, agency, credentialId, AChallenge, AChallengeToken);
end; end;
...@@ -174,8 +233,28 @@ procedure TFViewLogin.btnPasskeyLoginClick(Sender: TObject); ...@@ -174,8 +233,28 @@ procedure TFViewLogin.btnPasskeyLoginClick(Sender: TObject);
var var
credId: string; 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); procedure OnBeginOK(AChallenge, AChallengeToken: string);
begin begin
if AuthService.IsSimpleKey then
AuthService.LoginDeviceKey(credId, AChallengeToken, @OnLoginOK, @OnLoginError)
else
DoPasskeyAutoLogin(credId, AChallenge, AChallengeToken); DoPasskeyAutoLogin(credId, AChallenge, AChallengeToken);
end; 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