Commit 7fed875b by Mac Stephens

Add Twilio SMS verification, six-digit code validation, and simple-key device…

Add Twilio SMS verification, six-digit code validation, and simple-key device registration flow (removed twilio details from json for git)
parent 689bd1bf
......@@ -75,9 +75,12 @@ type
[HttpGet] function GetAgencyConfigList: TAgencyConfigList;
function VerifyVersion(ClientVersion: string): TJSONObject;
// WebAuthn registration — step 1: server checks phone pre-auth, returns challenge
// Section: Device registration step 1 checks phone pre-auth and sends an SMS code
function BeginRegistration(const PhoneNumber: string): TJSONObject;
// WebAuthn registration — step 2: client submits credential, server verifies + activates pending row
// Section: Device registration step 2 verifies the SMS code and returns a challenge
function VerifyRegistrationCode(const PhoneNumber, VerificationCode,
VerificationToken: string): TJSONObject;
// Section: WebAuthn registration verifies the credential and activates the pending row
function CompleteRegistration(const PhoneNumber, CredentialId,
AttestationObject, ClientDataJSON,
ChallengeToken: string): TJSONObject;
......
......@@ -28,6 +28,11 @@ type
function CheckUser(const User, Password, Agency: string): Integer;
function LoadUserByName(const User, Agency: string): Boolean;
function Decrypt(inStr, keyStr: AnsiString): AnsiString;
function GenerateVerificationCode: string;
function CreateVerificationToken(const PhoneNumber, Code: string): string;
function VerifyVerificationToken(const PhoneNumber, Code,
VerificationToken: string): Boolean;
procedure SendVerificationText(const PhoneNumber, Code: string);
// Returns True and sets AChallengeB64 if token is valid and of the expected type
function VerifyChallengeToken(const ChallengeToken, ExpectedType: string;
out AChallengeB64: string): Boolean;
......@@ -41,6 +46,8 @@ type
function GetAgencieslist(): TAgenciesList;
function GetAgencyConfiglist: TAgencyConfigList;
function BeginRegistration(const PhoneNumber: string): TJSONObject;
function VerifyRegistrationCode(const PhoneNumber, VerificationCode,
VerificationToken: string): TJSONObject;
function CompleteRegistration(const PhoneNumber, CredentialId,
AttestationObject, ClientDataJSON,
ChallengeToken: string): TJSONObject;
......@@ -60,8 +67,11 @@ implementation
uses
System.DateUtils,
System.Classes,
System.Generics.Collections,
System.NetEncoding,
System.Net.HttpClient,
System.Net.URLClient,
Bcl.JOSE.Core.Builder,
Bcl.JOSE.Core.JWT,
Aurelius.Global.Utils,
......@@ -194,16 +204,119 @@ begin
Result := '+1' + digits;
end;
function TAuthService.GenerateVerificationCode: string;
var
bytes: TBytes;
value: Cardinal;
begin
bytes := RandomBytes(4);
value := (Cardinal(bytes[0]) shl 24) or
(Cardinal(bytes[1]) shl 16) or
(Cardinal(bytes[2]) shl 8) or
Cardinal(bytes[3]);
Result := FormatFloat('000000', value mod 1000000);
// ADD THIS DELETE BEFORE DEPLOY
Logger.Log(3, '---TAuthService.GenerateVerificationCode initiated with Verification Code: ' + Result);
Logger.Log(3, '---TAuthService.GenerateVerificationCode End');
end;
function TAuthService.CreateVerificationToken(const PhoneNumber,
Code: string): string;
var
payload, expiry: string;
keyBytes, hmacBytes: TBytes;
begin
expiry := IntToStr(DateTimeToUnix(TTimeZone.Local.ToUniversalTime(IncMinute(Now, 5))));
payload := PhoneNumber + '|' + expiry + '|' + Base64UrlEncode(RandomBytes(16));
keyBytes := TEncoding.UTF8.GetBytes(ServerConfig.jwtTokenSecret);
hmacBytes := HMACSHA256Bytes(keyBytes, TEncoding.UTF8.GetBytes(payload + '|' + Code));
Result := Base64UrlEncode(TEncoding.UTF8.GetBytes(payload)) + '.' + Base64UrlEncode(hmacBytes);
end;
function TAuthService.VerifyVerificationToken(const PhoneNumber, Code,
VerificationToken: string): Boolean;
var
tokenParts, payloadParts: TArray<string>;
payload, expectedHmac: string;
expiry: Int64;
keyBytes, hmacBytes: TBytes;
begin
Result := False;
tokenParts := VerificationToken.Split(['.']);
if Length(tokenParts) <> 2 then Exit;
try
payload := TEncoding.UTF8.GetString(Base64UrlDecode(tokenParts[0]));
except
Exit;
end;
payloadParts := payload.Split(['|']);
if Length(payloadParts) <> 3 then Exit;
if payloadParts[0] <> PhoneNumber then Exit;
expiry := StrToInt64Def(payloadParts[1], 0);
if (expiry = 0) or
(DateTimeToUnix(TTimeZone.Local.ToUniversalTime(Now)) > expiry) then Exit;
keyBytes := TEncoding.UTF8.GetBytes(ServerConfig.jwtTokenSecret);
hmacBytes := HMACSHA256Bytes(keyBytes, TEncoding.UTF8.GetBytes(payload + '|' + Code));
expectedHmac := Base64UrlEncode(hmacBytes);
Result := expectedHmac = tokenParts[1];
end;
procedure TAuthService.SendVerificationText(const PhoneNumber, Code: string);
var
httpClient: THTTPClient;
bodyStream: TStringStream;
formData, authStr, msgBody: string;
response: IHTTPResponse;
begin
if (ServerConfig.twilioAccountSid = '') or
(ServerConfig.twilioAuthToken = '') or
(ServerConfig.twilioFromNumber = '') then
raise Exception.Create('Twilio is not configured on the server.');
msgBody := 'Your emiMobile verification code is: ' + Code + sLineBreak +
'Enter this code in the registration form to continue.';
authStr := TNetEncoding.Base64.Encode(
ServerConfig.twilioAccountSid + ':' + ServerConfig.twilioAuthToken)
.Replace(#13, '').Replace(#10, '');
formData := 'From=' + TNetEncoding.URL.Encode(ServerConfig.twilioFromNumber) +
'&To=' + TNetEncoding.URL.Encode(PhoneNumber) +
'&Body=' + TNetEncoding.URL.Encode(msgBody);
httpClient := THTTPClient.Create;
try
httpClient.ContentType := 'application/x-www-form-urlencoded';
httpClient.CustomHeaders['Authorization'] := 'Basic ' + authStr;
bodyStream := TStringStream.Create(formData, TEncoding.UTF8);
try
response := httpClient.Post(
'https://api.twilio.com/2010-04-01/Accounts/' +
ServerConfig.twilioAccountSid + '/Messages.json', bodyStream);
if response.StatusCode >= 300 then
raise Exception.CreateFmt('Twilio error %d: %s',
[response.StatusCode, response.ContentAsString]);
finally
bodyStream.Free;
end;
finally
httpClient.Free;
end;
end;
// ---------------------------------------------------------------------------
// BeginRegistration
// ---------------------------------------------------------------------------
function TAuthService.BeginRegistration(const PhoneNumber: string): TJSONObject;
var
token, challengeB64, normalizedPhone: string;
code, normalizedPhone, verificationToken, deviceName: string;
deviceId: Integer;
q: TUniQuery;
begin
Logger.Log(2, 'AuthService.BeginRegistration - phone: "' + PhoneNumber + '"');
Logger.Log(3, '---TAuthService.BeginRegistration initiated with phone number: ' + PhoneNumber);
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
......@@ -231,7 +344,7 @@ begin
try
q.Connection := authDB.ucLemsOCSO;
q.SQL.Text :=
'SELECT id FROM lems.device_registrations ' +
'SELECT id, device_name FROM lems.device_registrations ' +
'WHERE phone_number = :PHONE AND status = ''pending''';
q.ParamByName('PHONE').AsString := normalizedPhone;
q.Open;
......@@ -243,18 +356,103 @@ begin
Result.AddPair('message', 'Phone number not recognized. Contact your administrator.');
Exit;
end;
deviceId := q.FieldByName('id').AsInteger;
deviceName := q.FieldByName('device_name').AsString;
q.Close;
finally
q.Free;
end;
token := MakeChallengeToken('reg');
code := GenerateVerificationCode;
verificationToken := CreateVerificationToken(normalizedPhone, code);
try
SendVerificationText(normalizedPhone, code);
except
on E: Exception do
begin
Logger.Log(2, '--Error sending registration verification code: ' + E.Message);
Result.AddPair('status', 'error');
Result.AddPair('message', 'The verification code could not be sent. Please try again.');
Exit;
end;
end;
// ADD THIS DELETE BEFORE DEPLOY
Logger.Log(3, Format(
'Verification code set to %s for phone number: %s with device id %d and device name: %s',
[code, normalizedPhone, deviceId, deviceName]));
Result.AddPair('status', 'ok');
Result.AddPair('message', 'Verification code sent.');
Result.AddPair('verificationToken', verificationToken);
Logger.Log(3, '---TAuthService.BeginRegistration End');
end;
function TAuthService.VerifyRegistrationCode(const PhoneNumber,
VerificationCode, VerificationToken: string): TJSONObject;
var
normalizedPhone, token, challengeB64: string;
q: TUniQuery;
begin
// ADD THIS DELETE BEFORE DEPLOY
Logger.Log(3, Format(
'---TAuthService.VerifyRegistrationCode initiated with phone number: %s and verification code: %s',
[PhoneNumber, VerificationCode]));
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
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;
q.SQL.Text :=
'SELECT id FROM lems.device_registrations ' +
'WHERE phone_number = :PHONE AND status = ''pending''';
q.ParamByName('PHONE').AsString := normalizedPhone;
q.Open;
if q.IsEmpty then
begin
q.Close;
Result.AddPair('status', 'error');
Result.AddPair('message', 'Phone number not recognized. Contact your administrator.');
Exit;
end;
q.Close;
finally
q.Free;
end;
if (Length(Trim(VerificationCode)) <> 6) or
not VerifyVerificationToken(normalizedPhone, Trim(VerificationCode),
VerificationToken) then
begin
Logger.Log(2, '--Verification failed for phone number: ' + normalizedPhone);
Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired verification code.');
Exit;
end;
token := MakeChallengeToken('reg-' + normalizedPhone);
challengeB64 := token.Split([':'], 4)[0];
Result.AddPair('status', 'ok');
Result.AddPair('challenge', challengeB64);
Result.AddPair('challengeToken', token);
Result.AddPair('rpId', ServerConfig.rpId);
Result.AddPair('rpName', ServerConfig.rpName);
Logger.Log(3, '--Verification successful for phone number: ' + normalizedPhone);
Logger.Log(3, '---TAuthService.VerifyRegistrationCode End');
end;
// ---------------------------------------------------------------------------
......@@ -268,7 +466,7 @@ var
cdJsonBytes, attObjBytes, credIdBytes: TBytes;
cdJsonText: string;
cdJson: TJSONObject;
typeVal, challengeVal, originVal, effectiveRpId: string;
typeVal, challengeVal, originVal, effectiveRpId, normalizedPhone: string;
authData: TBytes;
rpIdHash, credId, pubKeyX, pubKeyY: TBytes;
flags: Byte;
......@@ -283,8 +481,20 @@ begin
Logger.Log(2, 'AuthService.CompleteRegistration - credId: ' + Copy(CredentialId, 1, 20) + '...');
try
normalizedPhone := NormalizePhoneE164(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Exit;
end;
end;
// 1. Verify challenge token
if not VerifyChallengeToken(ChallengeToken, 'reg', challengeB64) then
if not VerifyChallengeToken(ChallengeToken,
'reg-' + normalizedPhone, challengeB64) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired registration challenge.');
......@@ -401,19 +611,7 @@ begin
Exit;
end;
// 9. Normalize phone and activate the pending row
var normalizedPhone: string := '';
try
normalizedPhone := NormalizePhoneE164(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Exit;
end;
end;
// 9. Activate the pending row
q := TUniQuery.Create(nil);
try
q.Connection := authDB.ucLemsOCSO;
......@@ -985,30 +1183,31 @@ begin
Logger.Log(2, 'AuthService.CompleteRegistrationSimple - phone: "' + PhoneNumber + '"');
if not VerifyChallengeToken(ChallengeToken, 'reg', challengeB64) then
try
normalizedPhone := NormalizePhoneE164(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired registration challenge.');
Result.AddPair('message', E.Message);
Exit;
end;
end;
if Trim(DeviceKey) = '' then
if not VerifyChallengeToken(ChallengeToken,
'reg-' + normalizedPhone, challengeB64) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Device key is required.');
Result.AddPair('message', 'Invalid or expired registration challenge.');
Exit;
end;
try
normalizedPhone := NormalizePhoneE164(PhoneNumber);
except
on E: Exception do
if Trim(DeviceKey) = '' then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Result.AddPair('message', 'Device key is required.');
Exit;
end;
end;
q := TUniQuery.Create(nil);
try
......
......@@ -2,5 +2,8 @@
"url": "http://localhost:2001/emsys/emiMobile/",
"jwtTokenSecret": "super_secret0123super_secret4567",
"adminPassword": "whatisthisusedfor?",
"webAppFolder": "static"
"webAppFolder": "static",
"twilioAccountSid": "",
"twilioAuthToken": "",
"twilioFromNumber": ""
}
......@@ -16,6 +16,7 @@ type
TOnLoginSuccess = reference to procedure;
TOnLoginError = reference to procedure(AMsg: string);
TOnCodeSentSuccess = reference to procedure(AVerificationToken: string);
TOnBeginSuccess = reference to procedure(AChallenge, AChallengeToken: string);
TOnDeviceSuccess = reference to procedure;
TOnDeviceError = reference to procedure(AMsg: string);
......@@ -49,9 +50,13 @@ type
function IsSimpleKey: Boolean;
procedure ClearCredentialId;
// WebAuthn registration — two-step
// Section: Device registration verification
procedure BeginRegistration(APhoneNumber: string;
ASuccess: TOnBeginSuccess; AError: TOnDeviceError);
ASuccess: TOnCodeSentSuccess; AError: TOnDeviceError);
procedure VerifyRegistrationCode(APhoneNumber, AVerificationCode,
AVerificationToken: string; ASuccess: TOnBeginSuccess;
AError: TOnDeviceError);
// Section: WebAuthn registration
procedure CompleteRegistration(APhoneNumber, ACredentialId,
AAttestationObject, AClientDataJSON, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
......@@ -214,7 +219,40 @@ end;
// ---- WebAuthn registration ----
procedure TAuthService.BeginRegistration(APhoneNumber: string;
ASuccess: TOnBeginSuccess; AError: TOnDeviceError);
ASuccess: TOnCodeSentSuccess; AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
var
resp: JS.TJSObject;
verificationToken, status: string;
begin
resp := JS.TJSObject(Response.Result);
status := JS.toString(resp.Properties['status']);
if status = 'error' then
begin
AError(JS.toString(resp.Properties['message']));
Exit;
end;
verificationToken := JS.toString(resp.Properties['verificationToken']);
ASuccess(verificationToken);
end;
procedure OnError(Error: TXDataClientError);
begin
AError(Format('%s: %s', [Error.ErrorCode, Error.ErrorMessage]));
end;
begin
FClient.RawInvoke(
'IAuthService.BeginRegistration',
[APhoneNumber],
@OnLoad, @OnError
);
end;
procedure TAuthService.VerifyRegistrationCode(APhoneNumber,
AVerificationCode, AVerificationToken: string; ASuccess: TOnBeginSuccess;
AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
var
......@@ -240,8 +278,8 @@ procedure TAuthService.BeginRegistration(APhoneNumber: string;
begin
FClient.RawInvoke(
'IAuthService.BeginRegistration',
[APhoneNumber],
'IAuthService.VerifyRegistrationCode',
[APhoneNumber, AVerificationCode, AVerificationToken],
@OnLoad, @OnError
);
end;
......
......@@ -18,28 +18,48 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
TextHint = '(303) 555-1234'
WidthPercent = 100.000000000000000000
end
object chkUseWebAuthn: TWebCheckBox
object btnRegister: TWebButton
Left = 240
Top = 163
Width = 160
Height = 21
Caption = 'Use Passkey (WebAuthn)'
ElementID = 'view.devicereg.chkwebauthn'
Width = 121
Height = 25
Caption = 'Send Code'
ElementID = 'view.devicereg.btnregister'
HeightPercent = 100.000000000000000000
TabOrder = 1
WidthPercent = 100.000000000000000000
OnClick = btnRegisterClick
end
object btnRegister: TWebButton
object pnlVerification: TWebPanel
Left = 240
Top = 190
Top = 194
Width = 177
Height = 58
ElementID = 'view.devicereg.verification'
TabOrder = 2
object edtVerificationCode: TWebEdit
Left = 0
Top = 0
Width = 121
Height = 21
ElementID = 'view.devicereg.edtverificationcode'
HeightPercent = 100.000000000000000000
TabOrder = 0
TextHint = 'Six-digit code'
WidthPercent = 100.000000000000000000
end
object btnVerifyCode: TWebButton
Left = 0
Top = 27
Width = 121
Height = 25
Caption = 'Register This Device'
ElementID = 'view.devicereg.btnregister'
Caption = 'Verify and Register'
ElementID = 'view.devicereg.btnverify'
HeightPercent = 100.000000000000000000
TabOrder = 2
TabOrder = 1
WidthPercent = 100.000000000000000000
OnClick = btnRegisterClick
OnClick = btnVerifyCodeClick
end
end
object pnlMessage: TWebPanel
Left = 240
......@@ -47,7 +67,7 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
Width = 121
Height = 33
ElementID = 'view.devicereg.message'
TabOrder = 2
TabOrder = 3
object lblMessage: TWebLabel
Left = 16
Top = 11
......
......@@ -31,17 +31,10 @@
aria-label="Close"></button>
</div>
<p id="view.devicereg.desc-webauthn" class="text-muted small mb-3">
<p 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.
then click <strong>Send Code</strong>.
</p>
<div class="mb-3">
......@@ -53,6 +46,21 @@
autofocus>
</div>
<div id="view.devicereg.verification" class="mb-3 d-none">
<label class="form-label small text-muted">Verification code</label>
<input id="view.devicereg.edtverificationcode"
class="form-control mb-2"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
placeholder="Six-digit code">
<button id="view.devicereg.btnverify"
class="btn btn-primary w-100">
Verify and Register
</button>
</div>
<div class="mb-3">
<p class="text-muted small mb-1">Browser</p>
<p id="view.devicereg.useragent"
......@@ -60,28 +68,9 @@
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
class="btn btn-outline-primary w-100">
Send Code
</button>
</div>
......
......@@ -11,20 +11,25 @@ uses
type
TFViewDeviceRegistration = class(TWebForm)
edtPhoneNumber: TWebEdit;
chkUseWebAuthn: TWebCheckBox;
btnRegister: TWebButton;
pnlVerification: TWebPanel;
edtVerificationCode: TWebEdit;
btnVerifyCode: TWebButton;
pnlMessage: TWebPanel;
lblMessage: TWebLabel;
btnCloseNotification: TWebButton;
XDataWebClient: TXDataWebClient;
procedure btnRegisterClick(Sender: TObject);
procedure btnVerifyCodeClick(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
private
FRegistrationProc: TSuccessProc;
FVerificationToken: string;
procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
procedure HideNotification;
procedure SetBusy(ABusy: Boolean);
procedure SetVerifyBusy(ABusy: Boolean);
procedure DoWebAuthnCreate(APhoneNumber, AChallenge, AChallengeToken: string);
procedure DoSimpleKeyCreate(APhoneNumber, AChallengeToken: string);
public
......@@ -55,6 +60,8 @@ var
userAgent: string;
begin
HideNotification;
FVerificationToken := '';
pnlVerification.ElementHandle.classList.add('d-none');
userAgent := '';
asm
userAgent = navigator.userAgent;
......@@ -80,27 +87,6 @@ begin
});
}
// 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;
......@@ -108,14 +94,20 @@ 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;
if (ABusy) {
btn.textContent = (chk && chk.checked) ? 'Waiting for authenticator...' : 'Activating device...';
} else {
btn.textContent = 'Register This Device';
btn.textContent = ABusy ? 'Sending...' : 'Send Code';
}
end;
end;
procedure TFViewDeviceRegistration.SetVerifyBusy(ABusy: Boolean);
begin
asm
var btn = document.getElementById('view.devicereg.btnverify');
if (btn) {
btn.disabled = ABusy;
btn.textContent = ABusy ? 'Verifying...' : 'Verify and Register';
}
end;
end;
......@@ -123,17 +115,18 @@ end;
procedure TFViewDeviceRegistration.btnRegisterClick(Sender: TObject);
var
phoneNumber: string;
useWebAuthn: Boolean;
procedure OnBeginOK(AChallenge, AChallengeToken: string);
procedure OnCodeSent(AVerificationToken: string);
begin
if useWebAuthn then
DoWebAuthnCreate(phoneNumber, AChallenge, AChallengeToken)
else
DoSimpleKeyCreate(phoneNumber, AChallengeToken);
SetBusy(False);
FVerificationToken := AVerificationToken;
edtPhoneNumber.Enabled := False;
pnlVerification.ElementHandle.classList.remove('d-none');
edtVerificationCode.SetFocus;
ShowNotification('Verification code sent. Enter the six-digit code to continue.', False);
end;
procedure OnBeginError(AMsg: string);
procedure OnCodeError(AMsg: string);
begin
SetBusy(False);
ShowNotification(AMsg);
......@@ -147,11 +140,49 @@ begin
Exit;
end;
useWebAuthn := chkUseWebAuthn.Checked;
SetBusy(True);
HideNotification;
AuthService.BeginRegistration(phoneNumber, @OnBeginOK, @OnBeginError);
AuthService.BeginRegistration(phoneNumber, @OnCodeSent, @OnCodeError);
end;
procedure TFViewDeviceRegistration.btnVerifyCodeClick(Sender: TObject);
var
phoneNumber, verificationCode: string;
i: Integer;
procedure OnVerified(AChallenge, AChallengeToken: string);
begin
DoSimpleKeyCreate(phoneNumber, AChallengeToken);
end;
procedure OnVerifyError(AMsg: string);
begin
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
begin
phoneNumber := Trim(edtPhoneNumber.Text);
verificationCode := Trim(edtVerificationCode.Text);
if Length(verificationCode) <> 6 then
begin
ShowNotification('Please enter the six-digit verification code.');
Exit;
end;
for i := 1 to Length(verificationCode) do
if not CharInSet(verificationCode[i], ['0'..'9']) then
begin
ShowNotification('Please enter the six-digit verification code.');
Exit;
end;
SetVerifyBusy(True);
HideNotification;
AuthService.VerifyRegistrationCode(phoneNumber, verificationCode,
FVerificationToken, @OnVerified, @OnVerifyError);
end;
procedure TFViewDeviceRegistration.DoWebAuthnCreate(
......@@ -166,7 +197,7 @@ var
procedure OnCompleteError(AMsg: string);
begin
SetBusy(False);
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
......@@ -180,7 +211,7 @@ var
procedure OnWebAuthnError(AMsg: string);
begin
SetBusy(False);
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
......@@ -243,7 +274,7 @@ var
procedure OnCompleteError(AMsg: string);
begin
SetBusy(False);
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
......@@ -261,7 +292,7 @@ begin
if deviceKey = '' then
begin
SetBusy(False);
SetVerifyBusy(False);
ShowNotification('Failed to generate device key.');
Exit;
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