Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
emiMobile
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mac Stephens
emiMobile
Commits
f206e473
Commit
f206e473
authored
Sep 16, 2026
by
Michael Brachmann
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
simple key device registration option as an alternative to webauthn
parent
89533881
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
563 additions
and
7 deletions
+563
-7
Auth.Service.pas
emiMobileServer/Source/Auth.Service.pas
+8
-0
Auth.ServiceImpl.pas
emiMobileServer/Source/Auth.ServiceImpl.pas
+236
-0
device_registrations_key_type.sql
emiMobileServer/device_registrations_key_type.sql
+8
-0
Auth.Service.pas
webEMIMobile/Auth.Service.pas
+114
-1
View.DeviceRegistration.dfm
webEMIMobile/View.DeviceRegistration.dfm
+14
-1
View.DeviceRegistration.html
webEMIMobile/View.DeviceRegistration.html
+26
-1
View.DeviceRegistration.pas
webEMIMobile/View.DeviceRegistration.pas
+76
-2
View.Login.pas
webEMIMobile/View.Login.pas
+81
-2
No files found.
emiMobileServer/Source/Auth.Service.pas
View file @
f206e473
...
...
@@ -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
...
...
emiMobileServer/Source/Auth.ServiceImpl.pas
View file @
f206e473
...
...
@@ -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)
// ---------------------------------------------------------------------------
...
...
emiMobileServer/device_registrations_key_type.sql
0 → 100644
View file @
f206e473
-- 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'
;
webEMIMobile/Auth.Service.pas
View file @
f206e473
...
...
@@ -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
;
...
...
webEMIMobile/View.DeviceRegistration.dfm
View file @
f206e473
...
...
@@ -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
...
...
webEMIMobile/View.DeviceRegistration.html
View file @
f206e473
...
...
@@ -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
...
...
webEMIMobile/View.DeviceRegistration.pas
View file @
f206e473
...
...
@@ -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
;
...
...
webEMIMobile/View.Login.pas
View file @
f206e473
...
...
@@ -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,9 +178,25 @@ 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
DoWebAuthnGet
(
user
,
password
,
agency
,
credentialId
,
AChallenge
,
AChallengeToken
);
if
AuthService
.
IsSimpleKey
then
AuthService
.
LoginPasswordAndSimpleKey
(
user
,
password
,
agency
,
credentialId
,
AChallengeToken
,
@
OnLoginOK
,
@
OnLoginError
)
else
DoWebAuthnGet
(
user
,
password
,
agency
,
credentialId
,
AChallenge
,
AChallengeToken
);
end
;
procedure
OnBeginError
(
AMsg
:
string
);
...
...
@@ -174,9 +233,29 @@ 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
DoPasskeyAutoLogin
(
credId
,
AChallenge
,
AChallengeToken
);
if
AuthService
.
IsSimpleKey
then
AuthService
.
LoginDeviceKey
(
credId
,
AChallengeToken
,
@
OnLoginOK
,
@
OnLoginError
)
else
DoPasskeyAutoLogin
(
credId
,
AChallenge
,
AChallengeToken
);
end
;
procedure
OnBeginError
(
AMsg
:
string
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment