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
886d9a67
Commit
886d9a67
authored
Sep 10, 2026
by
Michael Brachmann
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
updates for auto login after passkey
parent
cf24b2b6
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
337 additions
and
8 deletions
+337
-8
Api.Service.pas
emiMobileServer/Source/Api.Service.pas
+1
-0
Api.ServiceImpl.pas
emiMobileServer/Source/Api.ServiceImpl.pas
+52
-2
Auth.Service.pas
emiMobileServer/Source/Auth.Service.pas
+8
-0
Auth.ServiceImpl.pas
emiMobileServer/Source/Auth.ServiceImpl.pas
+0
-0
device_registrations_username.sql
emiMobileServer/device_registrations_username.sql
+8
-0
Auth.Service.pas
webEMIMobile/Auth.Service.pas
+51
-0
View.DeviceManager.html
webEMIMobile/View.DeviceManager.html
+2
-1
View.DeviceManager.pas
webEMIMobile/View.DeviceManager.pas
+53
-4
View.Login.html
webEMIMobile/View.Login.html
+18
-0
View.Login.pas
webEMIMobile/View.Login.pas
+144
-1
No files found.
emiMobileServer/Source/Api.Service.pas
View file @
886d9a67
...
...
@@ -37,6 +37,7 @@ type
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
end
;
implementation
...
...
emiMobileServer/Source/Api.ServiceImpl.pas
View file @
886d9a67
...
...
@@ -43,6 +43,7 @@ type
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
end
;
implementation
...
...
@@ -1346,8 +1347,8 @@ begin
try
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
'SELECT id, credential_id, device_name, phone_number, user
_agent
, '
+
' registered_at, revoked_at, revoked_by, status '
+
'SELECT id, credential_id, device_name, phone_number, user
name
, '
+
'
user_agent,
registered_at, revoked_at, revoked_by, status '
+
'FROM lems.device_registrations '
+
'ORDER BY registered_at DESC'
;
q
.
Open
;
...
...
@@ -1361,6 +1362,7 @@ begin
item
.
credential_id
:=
q
.
FieldByName
(
'credential_id'
).
AsString
;
item
.
device_name
:=
q
.
FieldByName
(
'device_name'
).
AsString
;
item
.
phone_number
:=
q
.
FieldByName
(
'phone_number'
).
AsString
;
item
.
username
:=
q
.
FieldByName
(
'username'
).
AsString
;
item
.
user_agent
:=
q
.
FieldByName
(
'user_agent'
).
AsString
;
item
.
registered_at
:=
q
.
FieldByName
(
'registered_at'
).
AsString
;
if
q
.
FieldByName
(
'revoked_at'
).
IsNull
then
...
...
@@ -1725,6 +1727,54 @@ begin
Result
.
AddPair
(
'code'
,
redeemCode
);
end
;
function
TApiService
.
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
var
conn
:
TUniConnection
;
q
:
TUniQuery
;
begin
RequireAdmin
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
if
Trim
(
CredentialId
)
=
''
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'CredentialId is required.'
);
Exit
;
end
;
conn
:=
OpenLemsConnection
;
try
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
'UPDATE lems.device_registrations '
+
'SET username = :UNAME '
+
'WHERE credential_id = :CID AND status = ''active'''
;
q
.
ParamByName
(
'UNAME'
).
AsString
:=
Trim
(
Username
);
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
ExecSQL
;
if
q
.
RowsAffected
=
0
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device not found or not active.'
);
Exit
;
end
;
finally
q
.
Free
;
end
;
finally
conn
.
Free
;
end
;
Logger
.
Log
(
2
,
Format
(
'TApiService.UpdateDeviceUsername - set username "%s" on credId %s'
,
[
Trim
(
Username
),
Copy
(
CredentialId
,
1
,
20
)]));
Result
.
AddPair
(
'status'
,
'ok'
);
end
;
initialization
RegisterServiceType
(
TApiService
);
...
...
emiMobileServer/Source/Auth.Service.pas
View file @
886d9a67
...
...
@@ -46,6 +46,8 @@ type
credential_id
:
string
;
device_name
:
string
;
phone_number
:
string
;
username
:
string
;
agency
:
string
;
user_agent
:
string
;
registered_at
:
string
;
revoked_at
:
string
;
...
...
@@ -80,6 +82,12 @@ type
ChallengeToken
:
string
):
TJSONObject
;
// WebAuthn authentication challenge — called before Login
function
BeginAuthentication
(
const
CredentialId
:
string
):
TJSONObject
;
// Returns stored username/agency for a credential (unauthenticated; used by login form)
function
GetDeviceUser
(
const
CredentialId
:
string
):
TJSONObject
;
// Passkey-only login (no password) — uses stored username+agency from device record
function
LoginAutomatic
(
const
CredentialId
,
ChallengeToken
,
AuthenticatorData
,
ClientDataJSON
,
Signature
:
string
):
string
;
end
;
implementation
...
...
emiMobileServer/Source/Auth.ServiceImpl.pas
View file @
886d9a67
This diff is collapsed.
Click to expand it.
emiMobileServer/device_registrations_username.sql
0 → 100644
View file @
886d9a67
-- Migration: add username and agency to device_registrations
-- username: the CAD username of the person who last logged in from this device
-- (saved automatically on first successful login, or set manually by admin)
-- agency: the CAD agency used at login time (saved automatically; required for auto-login)
ALTER
TABLE
lems
.
device_registrations
ADD
COLUMN
IF
NOT
EXISTS
username
VARCHAR
(
50
),
ADD
COLUMN
IF
NOT
EXISTS
agency
VARCHAR
(
20
);
webEMIMobile/Auth.Service.pas
View file @
886d9a67
...
...
@@ -17,6 +17,7 @@ type
TOnBeginSuccess
=
reference
to
procedure
(
AChallenge
,
AChallengeToken
:
string
);
TOnDeviceSuccess
=
reference
to
procedure
;
TOnDeviceError
=
reference
to
procedure
(
AMsg
:
string
);
TOnGetDeviceUserOK
=
reference
to
procedure
(
AUsername
,
AAgency
:
string
);
TAuthService
=
class
private
...
...
@@ -54,6 +55,11 @@ type
procedure
LoginWithAssertion
(
AUser
,
APassword
,
AAgency
,
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
string
;
ASuccess
:
TOnLoginSuccess
;
AError
:
TOnLoginError
);
// Passkey-only auto-login (no password) — uses stored username on device record
procedure
GetDeviceUser
(
ACredentialId
:
string
;
ASuccess
:
TOnGetDeviceUserOK
);
procedure
LoginAutomatic
(
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
string
;
ASuccess
:
TOnLoginSuccess
;
AError
:
TOnLoginError
);
end
;
TJwtHelper
=
class
...
...
@@ -282,6 +288,51 @@ begin
);
end
;
// ---- Passkey auto-login ----
procedure
TAuthService
.
GetDeviceUser
(
ACredentialId
:
string
;
ASuccess
:
TOnGetDeviceUserOK
);
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
var
resp
:
JS
.
TJSObject
;
begin
resp
:=
JS
.
TJSObject
(
Response
.
Result
);
ASuccess
(
JS
.
toString
(
resp
.
Properties
[
'username'
]),
JS
.
toString
(
resp
.
Properties
[
'agency'
])
);
end
;
begin
FClient
.
RawInvoke
(
'IAuthService.GetDeviceUser'
,
[
ACredentialId
],
@
OnLoad
);
end
;
procedure
TAuthService
.
LoginAutomatic
(
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
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.LoginAutomatic'
,
[
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
],
@
OnLoad
,
@
OnError
);
end
;
// ---- Token helpers ----
function
TAuthService
.
TokenExpirationDate
:
TDateTime
;
...
...
webEMIMobile/View.DeviceManager.html
View file @
886d9a67
...
...
@@ -48,10 +48,11 @@
<tr>
<th
style=
"min-width:130px;"
>
Device Name
</th>
<th
style=
"min-width:120px;"
>
Phone
</th>
<th
style=
"min-width:140px;"
>
Username
</th>
<th>
Browser / User Agent
</th>
<th
style=
"min-width:135px;"
>
Registered
</th>
<th
style=
"min-width:80px;"
>
Status
</th>
<th
style=
"min-width:1
6
0px;"
>
Actions
</th>
<th
style=
"min-width:1
8
0px;"
>
Actions
</th>
</tr>
</thead>
<tbody
id=
"view.devmgr.tbody"
>
...
...
webEMIMobile/View.DeviceManager.pas
View file @
886d9a67
...
...
@@ -23,13 +23,14 @@ type
procedure
ShowNotification
(
const
AMsg
:
string
;
AIsError
:
Boolean
=
True
);
procedure
HideNotification
;
procedure
ClearTable
;
procedure
AddDeviceRow
(
const
ACredentialId
,
AName
,
APhoneNumber
,
procedure
AddDeviceRow
(
const
ACredentialId
,
AName
,
APhoneNumber
,
AUsername
,
AUserAgent
,
ARegisteredAt
,
AStatus
:
string
);
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
DeletePendingDevice
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
SendAppLink
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
AddPendingDevice
;
[
async
]
procedure
UpdateDeviceUsername
(
const
ACredentialId
,
AUsername
:
string
);
public
end
;
...
...
@@ -116,18 +117,19 @@ begin
end
;
procedure
TFViewDeviceManager
.
AddDeviceRow
(
const
ACredentialId
,
AName
,
APhoneNumber
,
AUserAgent
,
ARegisteredAt
,
AStatus
:
string
);
APhoneNumber
,
AUser
name
,
AUser
Agent
,
ARegisteredAt
,
AStatus
:
string
);
var
tbody
,
tr
,
tdName
,
tdPhone
,
tdAgent
,
tdReg
,
tdStatus
,
tdAction
:
TJSHTMLElement
;
tbody
,
tr
,
tdName
,
tdPhone
,
td
User
,
td
Agent
,
tdReg
,
tdStatus
,
tdAction
:
TJSHTMLElement
;
btn
,
btnSend
:
TJSHTMLElement
;
displayDate
,
displayPhone
:
string
;
isPending
,
isRevoked
:
Boolean
;
isPending
,
isRevoked
,
isActive
:
Boolean
;
begin
tbody
:=
TJSHTMLElement
(
document
.
getElementById
(
'view.devmgr.tbody'
));
if
not
Assigned
(
tbody
)
then
Exit
;
isPending
:=
AStatus
=
'pending'
;
isRevoked
:=
AStatus
=
'revoked'
;
isActive
:=
AStatus
=
'active'
;
displayPhone
:=
FormatPhoneDisplay
(
APhoneNumber
);
tr
:=
TJSHTMLElement
(
document
.
createElement
(
'tr'
));
...
...
@@ -147,6 +149,31 @@ begin
tdPhone
.
innerText
:=
displayPhone
;
tr
.
appendChild
(
tdPhone
);
// Username — editable for active devices
tdUser
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
isActive
then
begin
tdUser
.
innerHTML
:=
'<div class="d-flex gap-1 align-items-center">'
+
'<input type="text" class="form-control form-control-sm devmgr-username-input" '
+
'style="max-width:110px;" value="'
+
AUsername
+
'" '
+
'placeholder="username">'
+
'<button class="btn btn-outline-secondary btn-sm devmgr-username-save">Save</button>'
+
'</div>'
;
var
inp
:=
TJSHTMLElement
(
tdUser
.
querySelector
(
'.devmgr-username-input'
));
var
saveBtn
:=
TJSHTMLElement
(
tdUser
.
querySelector
(
'.devmgr-username-save'
));
if
Assigned
(
saveBtn
)
and
Assigned
(
inp
)
then
saveBtn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
begin
UpdateDeviceUsername
(
ACredentialId
,
string
(
TJSHTMLInputElement
(
inp
).
value
));
end
);
end
else
if
AUsername
<>
''
then
tdUser
.
innerText
:=
AUsername
else
tdUser
.
innerHTML
:=
'<em class="text-muted small">—</em>'
;
tr
.
appendChild
(
tdUser
);
// User agent (truncated)
tdAgent
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
not
isPending
then
...
...
@@ -263,6 +290,7 @@ begin
string
(
item
[
'credential_id'
]),
string
(
item
[
'device_name'
]),
string
(
item
[
'phone_number'
]),
string
(
item
[
'username'
]),
string
(
item
[
'user_agent'
]),
string
(
item
[
'registered_at'
]),
string
(
item
[
'status'
])
...
...
@@ -349,6 +377,27 @@ begin
AddPendingDevice
;
end
;
procedure
TFViewDeviceManager
.
UpdateDeviceUsername
(
const
ACredentialId
,
AUsername
:
string
);
var
resp
:
TXDataClientResponse
;
res
:
TJSObject
;
status
:
string
;
begin
try
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.UpdateDeviceUsername'
,
[
ACredentialId
,
AUsername
]));
res
:=
TJSObject
(
resp
.
Result
);
status
:=
string
(
res
[
'status'
]);
if
status
=
'ok'
then
ShowNotification
(
'Username updated.'
,
False
)
else
ShowNotification
(
string
(
res
[
'message'
]));
except
on
E
:
Exception
do
ShowNotification
(
'Update failed: '
+
E
.
Message
);
end
;
end
;
procedure
TFViewDeviceManager
.
AddPendingDevice
;
var
deviceName
,
phoneNumber
:
string
;
...
...
webEMIMobile/View.Login.html
View file @
886d9a67
...
...
@@ -29,6 +29,23 @@
aria-label=
"Close"
></button>
</div>
<!-- Auto-login section (shown when device has a stored username) -->
<div
id=
"view.login.autosection"
class=
"d-none"
>
<p
class=
"text-center text-muted mb-1 small"
>
Signing in as
</p>
<p
id=
"view.login.autouserlabel"
class=
"text-center fw-semibold fs-5 mb-3"
></p>
<button
id=
"view.login.btnpasskeylogin"
class=
"btn btn-primary w-100 mb-2"
>
Sign In with Passkey
</button>
<div
class=
"text-center"
>
<a
id=
"view.login.switchmanual"
href=
"#"
class=
"small text-muted"
>
Use a different account
</a>
</div>
</div>
<!-- Manual login section -->
<div
id=
"view.login.manualsection"
>
<div
class=
"mb-3"
>
<input
id=
"view.login.edtusername"
class=
"form-control"
...
...
@@ -52,6 +69,7 @@
Login
</button>
</div>
</div>
<div
class=
"card-footer text-muted small d-flex justify-content-between"
>
<span>
Please use your lems username
&
password to login.
</span>
<span
id=
"view.login.version"
class=
"opacity-75"
></span>
...
...
webEMIMobile/View.Login.pas
View file @
886d9a67
...
...
@@ -25,12 +25,15 @@ type
private
FLoginProc
:
TSuccessProc
;
FMessage
:
string
;
FAutoUsername
:
string
;
FAutoAgency
:
string
;
procedure
ShowNotification
(
Notification
:
string
);
procedure
HideNotification
;
procedure
GetAgencyConfigList
;
procedure
SetBusy
(
ABusy
:
Boolean
);
procedure
DoWebAuthnGet
(
AUser
,
APassword
,
AAgency
,
ACredentialId
,
AChallenge
,
AChallengeToken
:
string
);
procedure
DoPasskeyAutoLogin
(
ACredentialId
,
AChallenge
,
AChallengeToken
:
string
);
public
class
procedure
Display
(
LoginProc
:
TSuccessProc
);
overload
;
class
procedure
Display
(
LoginProc
:
TSuccessProc
;
AMsg
:
string
);
overload
;
...
...
@@ -69,16 +72,80 @@ end;
procedure
TFViewLogin
.
WebFormCreate
(
Sender
:
TObject
);
var
el
:
TJSElement
;
credId
:
string
;
procedure
OnDeviceUser
(
AUsername
,
AAgency
:
string
);
begin
FAutoUsername
:=
AUsername
;
FAutoAgency
:=
AAgency
;
if
AUsername
<>
''
then
begin
asm
var
autoSection
=
document
.
getElementById
(
'view.login.autosection'
);
var
manualSection
=
document
.
getElementById
(
'view.login.manualsection'
);
var
lbl
=
document
.
getElementById
(
'view.login.autouserlabel'
);
if
(
autoSection
)
autoSection
.
classList
.
remove
(
'd-none'
);
if
(
manualSection
)
manualSection
.
classList
.
add
(
'd-none'
);
if
(
lbl
)
lbl
.
textContent
=
AUsername
;
end
;
// Wire passkey button
asm
var
btn
=
document
.
getElementById
(
'view.login.btnpasskeylogin'
);
if
(
btn
)
{
btn.addEventListener('click', function() {
document.dispatchEvent(new CustomEvent('login-passkey'));
}
);
}
var
lnk
=
document
.
getElementById
(
'view.login.switchmanual'
);
if
(
lnk
)
{
lnk.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('view.login.autosection').classList.add('d-none');
document.getElementById('view.login.manualsection').classList.remove('d-none');
}
);
}
end
;
document
.
addEventListener
(
'login-passkey'
,
procedure
(
Event
:
TJSEvent
)
var
onBeginOK
:
TOnBeginSuccess
;
onBeginErr
:
TOnLoginError
;
storedCredId
:
string
;
begin
storedCredId
:=
AuthService
.
GetCredentialId
;
SetBusy
(
True
);
HideNotification
;
onBeginOK
:=
procedure
(
AChallenge
,
AChallengeToken
:
string
)
begin
DoPasskeyAutoLogin
(
storedCredId
,
AChallenge
,
AChallengeToken
);
end
;
onBeginErr
:=
procedure
(
AMsg
:
string
)
begin
SetBusy
(
False
);
ShowNotification
(
'Login Error: '
+
AMsg
);
end
;
AuthService
.
BeginAuthentication
(
storedCredId
,
onBeginOK
,
onBeginErr
);
end
);
end
;
end
;
begin
GetAgencyConfigList
;
el
:=
Document
.
getElementById
(
'view.login.version'
);
if
Assigned
(
el
)
then
TJSHtmlElement
(
el
).
innerText
:=
'v'
+
TDMConnection
.
clientVersion
;
GetAgencyConfigList
();
if
FMessage
<>
''
then
ShowNotification
(
FMessage
)
else
HideNotification
;
credId
:=
AuthService
.
GetCredentialId
;
if
credId
<>
''
then
AuthService
.
GetDeviceUser
(
credId
,
@
OnDeviceUser
);
end
;
procedure
TFViewLogin
.
SetBusy
(
ABusy
:
Boolean
);
...
...
@@ -208,6 +275,82 @@ begin
end
;
end
;
procedure
TFViewLogin
.
DoPasskeyAutoLogin
(
ACredentialId
,
AChallenge
,
AChallengeToken
:
string
);
var
credentialId
,
challenge
,
challengeToken
:
string
;
procedure
OnLoginOK
;
begin
FLoginProc
;
end
;
procedure
OnLoginError
(
AMsg
:
string
);
begin
SetBusy
(
False
);
ShowNotification
(
'Login Error: '
+
AMsg
);
// If auto-login fails, fall back to manual form
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
OnAssertion
(
AAuthData
,
AClientDataJSON
,
ASignature
:
string
);
begin
AuthService
.
LoginAutomatic
(
credentialId
,
challengeToken
,
AAuthData
,
AClientDataJSON
,
ASignature
,
@
OnLoginOK
,
@
OnLoginError
);
end
;
procedure
OnWebAuthnError
(
AMsg
:
string
);
begin
SetBusy
(
False
);
ShowNotification
(
'Passkey Error: '
+
AMsg
);
end
;
begin
credentialId
:=
ACredentialId
;
challenge
:=
AChallenge
;
challengeToken
:=
AChallengeToken
;
asm
(
function
()
{
function b64urlToArr(b64) {
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
var bin = atob(b64);
var arr = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return arr;
}
function
arrToB64url
(
buf
)
{
var bin = String.fromCharCode.apply(null, new Uint8Array(buf));
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
navigator
.
credentials
.
get
(
{
publicKey: {
challenge: b64urlToArr(challenge),
rpId: window.location.hostname,
allowCredentials: [{ id: b64urlToArr(credentialId), type: 'public-key' }
],
timeout
:
60000
,
userVerification
:
'required'
}
}).
then
(
function
(
assertion
)
{
var authData = arrToB64url(assertion.response.authenticatorData);
var cdJson = arrToB64url(assertion.response.clientDataJSON);
var sig = arrToB64url(assertion.response.signature);
OnAssertion(authData, cdJson, sig);
}
).
catch
(
function
(
err
)
{
OnWebAuthnError('WebAuthn error: ' + err.message);
}
);
})();
end
;
end
;
procedure
TFViewLogin
.
GetAgencyConfigList
;
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
...
...
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