mirror of
https://github.com/lukin/keywind.git
synced 2025-01-10 18:06:23 +00:00
implement webauthn and recovery codes from keycloak base
This commit is contained in:
parent
79b24d2b86
commit
0b1682031c
6 changed files with 558 additions and 1 deletions
176
theme/keywind/login/login-recovery-authn-code-config.ftl
Normal file
176
theme/keywind/login/login-recovery-authn-code-config.ftl
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<#import "components/button/primary.ftl" as buttonPrimary>
|
||||||
|
<#import "components/button/secondary.ftl" as buttonSecondary>
|
||||||
|
<#import "components/checkbox/primary.ftl" as checkboxPrimary>
|
||||||
|
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
|
||||||
|
<#if section = "header">
|
||||||
|
${kcSanitize(msg("recovery-code-config-header"))}
|
||||||
|
<#elseif section = "form">
|
||||||
|
|
||||||
|
<!-- warning -->
|
||||||
|
<div class="bg-orange-100 text-orange-600 p-4 rounded-lg text-sm" role="alert">
|
||||||
|
<p class="font-bold my-2">${kcSanitize(msg("recovery-code-config-warning-title"))}</p>
|
||||||
|
<span>${kcSanitize(msg("recovery-code-config-warning-message"))}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol class="font-mono kc-recovery-codes-list" id="kc-recovery-codes-list">
|
||||||
|
<#list recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList as code>
|
||||||
|
<li><span class="font-bold">${code?counter}:</span> ${code[0..3]}-${code[4..7]}-${code[8..]}</li>
|
||||||
|
</#list>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="flex items-stretch space-x-4 mb-4">
|
||||||
|
<button id="printRecoveryCodes" type="button" class="bg-secondary-100 flex justify-center px-2 py-1 relative rounded text-xs text-secondary-600 w-full focus:outline-none focus:ring-2 focus:ring-secondary-600 focus:ring-offset-2 hover:bg-secondary-200 hover:text-secondary-900">
|
||||||
|
${kcSanitize(msg("recovery-codes-print"))}
|
||||||
|
</button>
|
||||||
|
<button id="downloadRecoveryCodes" type="button" class="bg-secondary-100 flex justify-center px-2 py-1 relative rounded text-xs text-secondary-600 w-full focus:outline-none focus:ring-2 focus:ring-secondary-600 focus:ring-offset-2 hover:bg-secondary-200 hover:text-secondary-900">
|
||||||
|
${kcSanitize(msg("recovery-codes-download"))}
|
||||||
|
</button>
|
||||||
|
<button id="copyRecoveryCodes" type="button" class="bg-secondary-100 flex justify-center px-2 py-1 relative rounded text-xs text-secondary-600 w-full focus:outline-none focus:ring-2 focus:ring-secondary-600 focus:ring-offset-2 hover:bg-secondary-200 hover:text-secondary-900">
|
||||||
|
${kcSanitize(msg("recovery-codes-copy"))}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="${url.loginAction}" class="m-0 space-y-4m" method="post">
|
||||||
|
<input type="hidden" name="generatedRecoveryAuthnCodes" value="${recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesAsString}" />
|
||||||
|
<input type="hidden" name="generatedAt" value="${recoveryAuthnCodesConfigBean.generatedAt?c}" />
|
||||||
|
<input type="hidden" id="userLabel" name="userLabel" value="${msg("recovery-codes-label-default")}" />
|
||||||
|
|
||||||
|
<!-- confirmation checkbox -->
|
||||||
|
<@checkboxPrimary.kw id="kcRecoveryCodesConfirmationCheck" name="kcRecoveryCodesConfirmationCheck" required="required">
|
||||||
|
${kcSanitize(msg("recovery-codes-confirmation-message"))}
|
||||||
|
</@checkboxPrimary.kw>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-2 mt-4">
|
||||||
|
<#if isAppInitiatedAction??>
|
||||||
|
<@buttonPrimary.kw type="submit" id="saveRecoveryAuthnCodesBtn">
|
||||||
|
${kcSanitize(msg("recovery-codes-action-complete"))}
|
||||||
|
</@buttonPrimary.kw>
|
||||||
|
|
||||||
|
<@buttonSecondary.kw type="submit" id="cancelRecoveryAuthnCodesBtn" name="cancel-aia" value="true" onclick="document.getElementById('kcRecoveryCodesConfirmationCheck').required=false;return true;">
|
||||||
|
${kcSanitize(msg("recovery-codes-action-cancel"))}
|
||||||
|
</@buttonSecondary.kw>
|
||||||
|
<#else>
|
||||||
|
<@buttonPrimary.kw type="submit" id="saveRecoveryAuthnCodesBtn">
|
||||||
|
${kcSanitize(msg("recovery-codes-action-complete"))}
|
||||||
|
</@buttonPrimary.kw>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* copy recovery codes */
|
||||||
|
function copyRecoveryCodes() {
|
||||||
|
var tmpTextarea = document.createElement("textarea");
|
||||||
|
var codes = document.getElementById("kc-recovery-codes-list").getElementsByTagName("li");
|
||||||
|
for (i = 0; i < codes.length; i++) {
|
||||||
|
tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\n";
|
||||||
|
}
|
||||||
|
document.body.appendChild(tmpTextarea);
|
||||||
|
tmpTextarea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(tmpTextarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
var copyButton = document.getElementById("copyRecoveryCodes");
|
||||||
|
copyButton && copyButton.addEventListener("click", function () {
|
||||||
|
copyRecoveryCodes();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* download recovery codes */
|
||||||
|
function formatCurrentDateTime() {
|
||||||
|
var dt = new Date();
|
||||||
|
var options = {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short'
|
||||||
|
};
|
||||||
|
|
||||||
|
return dt.toLocaleString('en-US', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecoveryCodeList() {
|
||||||
|
var recoveryCodes = document.querySelectorAll(".kc-recovery-codes-list li");
|
||||||
|
var recoveryCodeList = "";
|
||||||
|
|
||||||
|
for (var i = 0; i < recoveryCodes.length; i++) {
|
||||||
|
var recoveryCodeLiElement = recoveryCodes[i].innerText;
|
||||||
|
recoveryCodeList += recoveryCodeLiElement + "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return recoveryCodeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDownloadContent() {
|
||||||
|
var recoveryCodeList = parseRecoveryCodeList();
|
||||||
|
var dt = new Date();
|
||||||
|
var options = {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fileBodyContent =
|
||||||
|
"${msg("recovery-codes-download-file-header")}\n\n" +
|
||||||
|
recoveryCodeList + "\n" +
|
||||||
|
"${msg("recovery-codes-download-file-description")}\n\n" +
|
||||||
|
"${msg("recovery-codes-download-file-date")} " + formatCurrentDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpDownloadLinkAndDownload(filename, text) {
|
||||||
|
var el = document.createElement('a');
|
||||||
|
el.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||||
|
el.setAttribute('download', filename);
|
||||||
|
el.style.display = 'none';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.click();
|
||||||
|
document.body.removeChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadRecoveryCodes() {
|
||||||
|
setUpDownloadLinkAndDownload('kc-download-recovery-codes.txt', buildDownloadContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadButton = document.getElementById("downloadRecoveryCodes");
|
||||||
|
downloadButton && downloadButton.addEventListener("click", downloadRecoveryCodes);
|
||||||
|
|
||||||
|
/* print recovery codes */
|
||||||
|
function buildPrintContent() {
|
||||||
|
var recoveryCodeListHTML = document.getElementById('kc-recovery-codes-list').innerHTML;
|
||||||
|
var styles =
|
||||||
|
`@page { size: auto; margin-top: 0; }
|
||||||
|
body { width: 480px; }
|
||||||
|
div { list-style-type: none; font-family: monospace }
|
||||||
|
p:first-of-type { margin-top: 48px }`
|
||||||
|
|
||||||
|
return printFileContent =
|
||||||
|
"<html><style>" + styles + "</style><body>" +
|
||||||
|
"<title>kc-download-recovery-codes</title>" +
|
||||||
|
"<p>${msg("recovery-codes-download-file-header")}</p>" +
|
||||||
|
"<div>" + recoveryCodeListHTML + "</div>" +
|
||||||
|
"<p>${msg("recovery-codes-download-file-description")}</p>" +
|
||||||
|
"<p>${msg("recovery-codes-download-file-date")} " + formatCurrentDateTime() + "</p>" +
|
||||||
|
"</body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function printRecoveryCodes() {
|
||||||
|
var w = window.open();
|
||||||
|
w.document.write(buildPrintContent());
|
||||||
|
w.print();
|
||||||
|
//w.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
var printButton = document.getElementById("printRecoveryCodes");
|
||||||
|
printButton && printButton.addEventListener("click", printRecoveryCodes);
|
||||||
|
</script>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
26
theme/keywind/login/login-recovery-authn-code-input.ftl
Normal file
26
theme/keywind/login/login-recovery-authn-code-input.ftl
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<#import "components/button/primary.ftl" as buttonPrimary>
|
||||||
|
<#import "components/input/primary.ftl" as inputPrimary>
|
||||||
|
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section="header">
|
||||||
|
${kcSanitize(msg("auth-recovery-code-header"))}
|
||||||
|
<#elseif section = "form">
|
||||||
|
<form class="m-0 space-y-4" action="${url.loginAction}" method="post">
|
||||||
|
<@inputPrimary.kw
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus=true
|
||||||
|
invalid=["firstName"]
|
||||||
|
name="recoveryCodeInput"
|
||||||
|
type="text"
|
||||||
|
value=(register.formData.firstName)!''
|
||||||
|
>
|
||||||
|
${msg("auth-recovery-code-prompt", recoveryAuthnCodesInputBean.codeNumber?c)}
|
||||||
|
</@inputPrimary.kw>
|
||||||
|
|
||||||
|
<@buttonPrimary.kw type="submit" name="login">
|
||||||
|
${kcSanitize(msg("doLogIn"))}
|
||||||
|
</@buttonPrimary.kw>
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
2
theme/keywind/login/resources/dist/index.css
vendored
2
theme/keywind/login/resources/dist/index.css
vendored
File diff suppressed because one or more lines are too long
130
theme/keywind/login/webauthn-authenticate.ftl
Normal file
130
theme/keywind/login/webauthn-authenticate.ftl
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<#import "components/button/primary.ftl" as buttonPrimary>
|
||||||
|
<#import "components/button/secondary.ftl" as buttonSecondary>
|
||||||
|
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section="header">
|
||||||
|
${msg("webauthn-login-title")}
|
||||||
|
|
||||||
|
<#elseif section = "form">
|
||||||
|
<div class="m-0">
|
||||||
|
<form id="webauth" action="${url.loginAction}" method="post">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
|
||||||
|
<input type="hidden" id="authenticatorData" name="authenticatorData"/>
|
||||||
|
<input type="hidden" id="signature" name="signature"/>
|
||||||
|
<input type="hidden" id="credentialId" name="credentialId"/>
|
||||||
|
<input type="hidden" id="userHandle" name="userHandle"/>
|
||||||
|
<input type="hidden" id="error" name="error"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<#if authenticators??>
|
||||||
|
<form id="authn_select" class="m-0">
|
||||||
|
<#list authenticators.authenticators as authenticator>
|
||||||
|
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
|
||||||
|
</#list>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
|
||||||
|
<#if authenticators.authenticators?size gt 1>
|
||||||
|
<p class="font-bold py-2 text-xl">${kcSanitize(msg("webauthn-available-authenticators"))?no_esc}</p>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#list authenticators.authenticators as authenticator>
|
||||||
|
<div class="my-5">
|
||||||
|
<p><span class="font-bold">${kcSanitize(msg('${authenticator.label}'))?no_esc}</span>
|
||||||
|
<#--<#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content>
|
||||||
|
(<#list authenticator.transports.displayNameProperties as nameProperty>
|
||||||
|
${kcSanitize(msg('${nameProperty!}'))?no_esc}
|
||||||
|
<#if nameProperty?has_next>, </#if>
|
||||||
|
</#list>)
|
||||||
|
</#if>-->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>${kcSanitize(msg('webauthn-createdAt-label'))?no_esc}: <span class="font-bold">${kcSanitize(authenticator.createdAt)?no_esc}</span></p>
|
||||||
|
</div>
|
||||||
|
</#list>
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<@buttonPrimary.kw type="button" onclick="webAuthnAuthenticate()" autofocus="autofocus">
|
||||||
|
${msg("webauthn-doAuthenticate")}
|
||||||
|
</@buttonPrimary.kw>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function webAuthnAuthenticate() {
|
||||||
|
let isUserIdentified = ${isUserIdentified};
|
||||||
|
if (!isUserIdentified) {
|
||||||
|
doAuthenticate([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkAllowCredentials();
|
||||||
|
}
|
||||||
|
function checkAllowCredentials() {
|
||||||
|
let allowCredentials = [];
|
||||||
|
let authn_use = document.forms['authn_select'].authn_use_chk;
|
||||||
|
if (authn_use !== undefined) {
|
||||||
|
if (authn_use.length === undefined) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.decode(authn_use.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < authn_use.length; i++) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.decode(authn_use[i].value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doAuthenticate(allowCredentials);
|
||||||
|
}
|
||||||
|
function doAuthenticate(allowCredentials) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
jQuery("#error").val("${msg("webauthn-unsupported-browser-text")?no_esc}");
|
||||||
|
jQuery("#webauth").submit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let challenge = "${challenge}";
|
||||||
|
let userVerification = "${userVerification}";
|
||||||
|
let rpId = "${rpId}";
|
||||||
|
let publicKey = {
|
||||||
|
rpId : rpId,
|
||||||
|
challenge: base64url.decode(challenge, { loose: true })
|
||||||
|
};
|
||||||
|
let createTimeout = ${createTimeout};
|
||||||
|
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
|
||||||
|
if (allowCredentials.length) {
|
||||||
|
publicKey.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
|
||||||
|
navigator.credentials.get({publicKey})
|
||||||
|
.then((result) => {
|
||||||
|
window.result = result;
|
||||||
|
let clientDataJSON = result.response.clientDataJSON;
|
||||||
|
let authenticatorData = result.response.authenticatorData;
|
||||||
|
let signature = result.response.signature;
|
||||||
|
jQuery("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
|
||||||
|
jQuery("#authenticatorData").val(base64url.encode(new Uint8Array(authenticatorData), { pad: false }));
|
||||||
|
jQuery("#signature").val(base64url.encode(new Uint8Array(signature), { pad: false }));
|
||||||
|
jQuery("#credentialId").val(result.id);
|
||||||
|
if(result.response.userHandle) {
|
||||||
|
jQuery("#userHandle").val(base64url.encode(new Uint8Array(result.response.userHandle), { pad: false }));
|
||||||
|
}
|
||||||
|
jQuery("#webauth").submit();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
jQuery("#error").val(err);
|
||||||
|
jQuery("#webauth").submit();
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<#elseif section = "info">
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
36
theme/keywind/login/webauthn-error.ftl
Normal file
36
theme/keywind/login/webauthn-error.ftl
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<#import "components/button/primary.ftl" as buttonPrimary>
|
||||||
|
<#import "components/button/secondary.ftl" as buttonSecondary>
|
||||||
|
|
||||||
|
<@layout.registrationLayout displayMessage=true; section>
|
||||||
|
<#if section = "header">
|
||||||
|
${kcSanitize(msg("webauthn-error-title"))}
|
||||||
|
<#elseif section = "form">
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
refreshPage = () => {
|
||||||
|
document.getElementById('isSetRetry').value = 'retry';
|
||||||
|
document.getElementById('executionValue').value = '${execution}';
|
||||||
|
document.getElementById('kc-error-credential-form').submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form action="${url.loginAction}" method="post" class="m-0">
|
||||||
|
<input type="hidden" id="executionValue" name="authenticationExecution"/>
|
||||||
|
<input type="hidden" id="isSetRetry" name="isSetRetry"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<@buttonPrimary.kw type="button" tabindex="4" onclick="refreshPage()">
|
||||||
|
${kcSanitize(msg("doTryAgain"))}
|
||||||
|
</@buttonPrimary.kw>
|
||||||
|
|
||||||
|
<#if isAppInitiatedAction??>
|
||||||
|
<form action="${url.loginAction}" method="post" class="m-0">
|
||||||
|
<@buttonSecondary.kw name="cancel-aia" type="submit" value="true">
|
||||||
|
${kcSanitize(msg("doCancel"))}
|
||||||
|
</@buttonSecondary.kw>
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
189
theme/keywind/login/webauthn-register.ftl
Normal file
189
theme/keywind/login/webauthn-register.ftl
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<#import "components/button/primary.ftl" as buttonPrimary>
|
||||||
|
<#import "components/button/secondary.ftl" as buttonSecondary>
|
||||||
|
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section="header">
|
||||||
|
${kcSanitize(msg("webauthn-registration-title"))}
|
||||||
|
<#elseif section = "form">
|
||||||
|
|
||||||
|
<form id="register" action="${url.loginAction}" class="m-0 space-y-4" method="post">
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
|
||||||
|
<input type="hidden" id="attestationObject" name="attestationObject"/>
|
||||||
|
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
|
||||||
|
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
|
||||||
|
<input type="hidden" id="transports" name="transports"/>
|
||||||
|
<input type="hidden" id="error" name="error"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function registerSecurityKey() {
|
||||||
|
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
jQuery("#error").val("${msg("webauthn-unsupported-browser-text")?no_esc}");
|
||||||
|
jQuery("#register").submit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mandatory parameters
|
||||||
|
let challenge = "${challenge}";
|
||||||
|
let userid = "${userid}";
|
||||||
|
let username = "${username}";
|
||||||
|
|
||||||
|
let signatureAlgorithms = "${signatureAlgorithms}";
|
||||||
|
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
|
||||||
|
|
||||||
|
let rpEntityName = "${rpEntityName}";
|
||||||
|
let rp = {name: rpEntityName};
|
||||||
|
|
||||||
|
let publicKey = {
|
||||||
|
challenge: base64url.decode(challenge, {loose: true}),
|
||||||
|
rp: rp,
|
||||||
|
user: {
|
||||||
|
id: base64url.decode(userid, {loose: true}),
|
||||||
|
name: username,
|
||||||
|
displayName: username
|
||||||
|
},
|
||||||
|
pubKeyCredParams: pubKeyCredParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
// optional parameters
|
||||||
|
let rpId = "${rpId}";
|
||||||
|
publicKey.rp.id = rpId;
|
||||||
|
|
||||||
|
let attestationConveyancePreference = "${attestationConveyancePreference}";
|
||||||
|
if (attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
|
||||||
|
|
||||||
|
let authenticatorSelection = {};
|
||||||
|
let isAuthenticatorSelectionSpecified = false;
|
||||||
|
|
||||||
|
let authenticatorAttachment = "${authenticatorAttachment}";
|
||||||
|
if (authenticatorAttachment !== 'not specified') {
|
||||||
|
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
isAuthenticatorSelectionSpecified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requireResidentKey = "${requireResidentKey}";
|
||||||
|
if (requireResidentKey !== 'not specified') {
|
||||||
|
if (requireResidentKey === 'Yes')
|
||||||
|
authenticatorSelection.requireResidentKey = true;
|
||||||
|
else
|
||||||
|
authenticatorSelection.requireResidentKey = false;
|
||||||
|
isAuthenticatorSelectionSpecified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userVerificationRequirement = "${userVerificationRequirement}";
|
||||||
|
if (userVerificationRequirement !== 'not specified') {
|
||||||
|
authenticatorSelection.userVerification = userVerificationRequirement;
|
||||||
|
isAuthenticatorSelectionSpecified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
|
||||||
|
|
||||||
|
let createTimeout = ${createTimeout};
|
||||||
|
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
|
||||||
|
|
||||||
|
let excludeCredentialIds = "${excludeCredentialIds}";
|
||||||
|
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
|
||||||
|
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
|
||||||
|
|
||||||
|
navigator.credentials.create({publicKey})
|
||||||
|
.then(function (result) {
|
||||||
|
window.result = result;
|
||||||
|
let clientDataJSON = result.response.clientDataJSON;
|
||||||
|
let attestationObject = result.response.attestationObject;
|
||||||
|
let publicKeyCredentialId = result.rawId;
|
||||||
|
|
||||||
|
jQuery("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), {pad: false}));
|
||||||
|
jQuery("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
|
||||||
|
jQuery("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), {pad: false}));
|
||||||
|
|
||||||
|
if (typeof result.response.getTransports === "function") {
|
||||||
|
let transports = result.response.getTransports();
|
||||||
|
if (transports) {
|
||||||
|
jQuery("#transports").val(getTransportsAsString(transports));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Your browser is not able to recognize supported transport media for the authenticator.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let initLabel = "WebAuthn Authenticator (Default Label)";
|
||||||
|
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
|
||||||
|
if (labelResult === null) labelResult = initLabel;
|
||||||
|
jQuery("#authenticatorLabel").val(labelResult);
|
||||||
|
|
||||||
|
jQuery("#register").submit();
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
jQuery("#error").val(err);
|
||||||
|
jQuery("#register").submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPubKeyCredParams(signatureAlgorithms) {
|
||||||
|
let pubKeyCredParams = [];
|
||||||
|
if (signatureAlgorithms === "") {
|
||||||
|
pubKeyCredParams.push({type: "public-key", alg: -7});
|
||||||
|
return pubKeyCredParams;
|
||||||
|
}
|
||||||
|
let signatureAlgorithmsList = signatureAlgorithms.split(',');
|
||||||
|
|
||||||
|
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
|
||||||
|
pubKeyCredParams.push({
|
||||||
|
type: "public-key",
|
||||||
|
alg: signatureAlgorithmsList[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pubKeyCredParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExcludeCredentials(excludeCredentialIds) {
|
||||||
|
let excludeCredentials = [];
|
||||||
|
if (excludeCredentialIds === "") return excludeCredentials;
|
||||||
|
|
||||||
|
let excludeCredentialIdsList = excludeCredentialIds.split(',');
|
||||||
|
|
||||||
|
for (let i = 0; i < excludeCredentialIdsList.length; i++) {
|
||||||
|
excludeCredentials.push({
|
||||||
|
type: "public-key",
|
||||||
|
id: base64url.decode(excludeCredentialIdsList[i],
|
||||||
|
{loose: true})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return excludeCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransportsAsString(transportsList) {
|
||||||
|
if (transportsList === '' || transportsList.constructor !== Array) return "";
|
||||||
|
|
||||||
|
let transportsString = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < transportsList.length; i++) {
|
||||||
|
transportsString += transportsList[i] + ",";
|
||||||
|
}
|
||||||
|
|
||||||
|
return transportsString.slice(0, -1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<@buttonPrimary.kw type="submit" onclick="registerSecurityKey()">
|
||||||
|
${kcSanitize(msg("doRegister"))}
|
||||||
|
</@buttonPrimary.kw>
|
||||||
|
|
||||||
|
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
||||||
|
<form action="${url.loginAction}" method="post" class="m-0">
|
||||||
|
<@buttonSecondary.kw name="cancel-aia" type="submit" value="true">
|
||||||
|
${kcSanitize(msg("doCancel"))}
|
||||||
|
</@buttonSecondary.kw>
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
Loading…
Reference in a new issue