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;
......
......@@ -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,36 +18,56 @@ 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)'
ElementID = 'view.devicereg.chkwebauthn'
HeightPercent = 100.000000000000000000
TabOrder = 1
WidthPercent = 100.000000000000000000
end
object btnRegister: TWebButton
Left = 240
Top = 190
Top = 163
Width = 121
Height = 25
Caption = 'Register This Device'
Caption = 'Send Code'
ElementID = 'view.devicereg.btnregister'
HeightPercent = 100.000000000000000000
TabOrder = 2
TabOrder = 1
WidthPercent = 100.000000000000000000
OnClick = btnRegisterClick
end
object pnlVerification: TWebPanel
Left = 240
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 = 'Verify and Register'
ElementID = 'view.devicereg.btnverify'
HeightPercent = 100.000000000000000000
TabOrder = 1
WidthPercent = 100.000000000000000000
OnClick = btnVerifyCodeClick
end
end
object pnlMessage: TWebPanel
Left = 240
Top = 65
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