Refactoring JavaScript code of WebAuthn's authenticators to follow the current Keycloak's JavaScript coding convention
closes #26713 Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
parent
269027571a
commit
d5bf79b932
11 changed files with 289 additions and 361 deletions
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.webauthn.pages;
|
|||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
|
@ -48,7 +49,7 @@ public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
|||
public boolean isCurrent() {
|
||||
try {
|
||||
authenticateButton.getText();
|
||||
return driver.getPageSource().contains("navigator.credentials.get");
|
||||
return driver.findElement(By.id("authenticateWebAuthnButton")).isDisplayed();
|
||||
} catch (NoSuchElementException e) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.hamcrest.CoreMatchers;
|
|||
import org.keycloak.testsuite.pages.LogoutSessionsPage;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.openqa.selenium.Alert;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.TimeoutException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
@ -85,7 +86,7 @@ public class WebAuthnRegisterPage extends LogoutSessionsPage {
|
|||
// label edit after registering authenticator by .create()
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(seconds));
|
||||
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
||||
assertThat(promptDialog.getText(), CoreMatchers.is("Please input your registered authenticator's label"));
|
||||
assertThat(promptDialog.getText(), CoreMatchers.is("Please input your registered passkey's label"));
|
||||
return true;
|
||||
} catch (TimeoutException e) {
|
||||
return false;
|
||||
|
@ -115,7 +116,7 @@ public class WebAuthnRegisterPage extends LogoutSessionsPage {
|
|||
public boolean isCurrent() {
|
||||
final String formTitle = getFormTitle();
|
||||
return formTitle != null && formTitle.equals("Passkey Registration") &&
|
||||
driver.getPageSource().contains("navigator.credentials.create");
|
||||
driver.findElement(By.id("registerWebAuthn")).isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -485,6 +485,9 @@ webauthn-available-authenticators=Available Passkeys
|
|||
webauthn-unsupported-browser-text=WebAuthn is not supported by this browser. Try another one or contact your administrator.
|
||||
webauthn-doAuthenticate=Sign in with Passkey
|
||||
webauthn-createdAt-label=Created
|
||||
webauthn-registration-init-label=Passkey (Default Label)
|
||||
webauthn-registration-init-label-prompt=Please input your registered passkey''s label
|
||||
|
||||
|
||||
# WebAuthn Error
|
||||
webauthn-error-title=Passkey Error
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
// for embedded scripts, quoted and modified from https://github.com/swansontec/rfc4648.js by William Swanson
|
||||
'use strict';
|
||||
var base64url = base64url || {};
|
||||
(function(base64url) {
|
||||
|
||||
function parse (string, encoding, opts = {}) {
|
||||
// Build the character lookup table:
|
||||
if (!encoding.codes) {
|
||||
encoding.codes = {};
|
||||
for (let i = 0; i < encoding.chars.length; ++i) {
|
||||
encoding.codes[encoding.chars[i]] = i;
|
||||
}
|
||||
}
|
||||
|
||||
// The string must have a whole number of bytes:
|
||||
if (!opts.loose && (string.length * encoding.bits) & 7) {
|
||||
throw new SyntaxError('Invalid padding');
|
||||
}
|
||||
|
||||
// Count the padding bytes:
|
||||
let end = string.length;
|
||||
while (string[end - 1] === '=') {
|
||||
--end;
|
||||
|
||||
// If we get a whole number of bytes, there is too much padding:
|
||||
if (!opts.loose && !(((string.length - end) * encoding.bits) & 7)) {
|
||||
throw new SyntaxError('Invalid padding');
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the output:
|
||||
const out = new (opts.out || Uint8Array)(((end * encoding.bits) / 8) | 0);
|
||||
|
||||
// Parse the data:
|
||||
let bits = 0; // Number of bits currently in the buffer
|
||||
let buffer = 0; // Bits waiting to be written out, MSB first
|
||||
let written = 0; // Next byte to write
|
||||
for (let i = 0; i < end; ++i) {
|
||||
// Read one character from the string:
|
||||
const value = encoding.codes[string[i]];
|
||||
if (value === void 0) {
|
||||
throw new SyntaxError('Invalid character ' + string[i]);
|
||||
}
|
||||
|
||||
// Append the bits to the buffer:
|
||||
buffer = (buffer << encoding.bits) | value;
|
||||
bits += encoding.bits;
|
||||
|
||||
// Write out some bits if the buffer has a byte's worth:
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[written++] = 0xff & (buffer >> bits);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that we have received just enough bits:
|
||||
if (bits >= encoding.bits || 0xff & (buffer << (8 - bits))) {
|
||||
throw new SyntaxError('Unexpected end of data');
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
function stringify (data, encoding, opts = {}) {
|
||||
const { pad = true } = opts;
|
||||
const mask = (1 << encoding.bits) - 1;
|
||||
let out = '';
|
||||
|
||||
let bits = 0; // Number of bits currently in the buffer
|
||||
let buffer = 0; // Bits waiting to be written out, MSB first
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
// Slurp data into the buffer:
|
||||
buffer = (buffer << 8) | (0xff & data[i]);
|
||||
bits += 8;
|
||||
|
||||
// Write out as much as we can:
|
||||
while (bits > encoding.bits) {
|
||||
bits -= encoding.bits;
|
||||
out += encoding.chars[mask & (buffer >> bits)];
|
||||
}
|
||||
}
|
||||
|
||||
// Partial character:
|
||||
if (bits) {
|
||||
out += encoding.chars[mask & (buffer << (encoding.bits - bits))];
|
||||
}
|
||||
|
||||
// Add padding characters until we hit a byte boundary:
|
||||
if (pad) {
|
||||
while ((out.length * encoding.bits) & 7) {
|
||||
out += '=';
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const encoding = {
|
||||
chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
|
||||
bits: 6
|
||||
}
|
||||
|
||||
base64url.decode = function (string, opts) {
|
||||
return parse(string, encoding, opts);
|
||||
}
|
||||
|
||||
base64url.encode = function (data, opts) {
|
||||
return stringify(data, encoding, opts)
|
||||
}
|
||||
|
||||
return base64url;
|
||||
}(base64url));
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { base64url } from "rfc4648";
|
||||
|
||||
export async function authenticateByWebAuthn(input) {
|
||||
if (!input.isUserIdentified) {
|
||||
try {
|
||||
const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
|
||||
returnSuccess(result);
|
||||
} catch (error) {
|
||||
returnFailure(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
|
||||
}
|
||||
|
||||
async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
|
||||
const allowCredentials = [];
|
||||
const authnUse = document.forms['authn_select'].authn_use_chk;
|
||||
if (authnUse !== undefined) {
|
||||
if (authnUse.length === undefined) {
|
||||
allowCredentials.push({
|
||||
id: base64url.parse(authnUse.value, {loose: true}),
|
||||
type: 'public-key',
|
||||
});
|
||||
} else {
|
||||
authnUse.forEach((entry) =>
|
||||
allowCredentials.push({
|
||||
id: base64url.parse(entry.value, {loose: true}),
|
||||
type: 'public-key',
|
||||
}));
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
|
||||
returnSuccess(result);
|
||||
} catch (error) {
|
||||
returnFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
|
||||
// Check if WebAuthn is supported by this browser
|
||||
if (!window.PublicKeyCredential) {
|
||||
returnFailure(errmsg);
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = {
|
||||
rpId : rpId,
|
||||
challenge: base64url.parse(challenge, { loose: true })
|
||||
};
|
||||
|
||||
if (createTimeout !== 0) {
|
||||
publicKey.timeout = createTimeout * 1000;
|
||||
}
|
||||
|
||||
if (allowCredentials.length) {
|
||||
publicKey.allowCredentials = allowCredentials;
|
||||
}
|
||||
|
||||
if (userVerification !== 'not specified') {
|
||||
publicKey.userVerification = userVerification;
|
||||
}
|
||||
|
||||
return navigator.credentials.get({publicKey});
|
||||
}
|
||||
|
||||
function returnSuccess(result) {
|
||||
document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), { pad: false });
|
||||
document.getElementById("authenticatorData").value = base64url.stringify(new Uint8Array(result.response.authenticatorData), { pad: false });
|
||||
document.getElementById("signature").value = base64url.stringify(new Uint8Array(result.response.signature), { pad: false });
|
||||
document.getElementById("credentialId").value = result.id;
|
||||
if (result.response.userHandle) {
|
||||
document.getElementById("userHandle").value = base64url.stringify(new Uint8Array(result.response.userHandle), { pad: false });
|
||||
}
|
||||
document.getElementById("webauth").submit();
|
||||
}
|
||||
|
||||
function returnFailure(err) {
|
||||
document.getElementById("error").value = err;
|
||||
document.getElementById("webauth").submit();
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import { base64url } from "rfc4648";
|
||||
|
||||
export async function registerByWebAuthn(input) {
|
||||
|
||||
// Check if WebAuthn is supported by this browser
|
||||
if (!window.PublicKeyCredential) {
|
||||
returnFailure(input.errmsg);
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = {
|
||||
challenge: base64url.parse(input.challenge, {loose: true}),
|
||||
rp: {id: input.rpId, name: input.rpEntityName},
|
||||
user: {
|
||||
id: base64url.parse(input.userid, {loose: true}),
|
||||
name: input.username,
|
||||
displayName: input.username
|
||||
},
|
||||
pubKeyCredParams: getPubKeyCredParams(input.signatureAlgorithms),
|
||||
};
|
||||
|
||||
if (input.attestationConveyancePreference !== 'not specified') {
|
||||
publicKey.attestation = input.attestationConveyancePreference;
|
||||
}
|
||||
|
||||
const authenticatorSelection = {};
|
||||
let isAuthenticatorSelectionSpecified = false;
|
||||
|
||||
if (input.authenticatorAttachment !== 'not specified') {
|
||||
authenticatorSelection.authenticatorAttachment = input.authenticatorAttachment;
|
||||
isAuthenticatorSelectionSpecified = true;
|
||||
}
|
||||
|
||||
if (input.requireResidentKey !== 'not specified') {
|
||||
if (input.requireResidentKey === 'Yes') {
|
||||
authenticatorSelection.requireResidentKey = true;
|
||||
} else {
|
||||
authenticatorSelection.requireResidentKey = false;
|
||||
}
|
||||
isAuthenticatorSelectionSpecified = true;
|
||||
}
|
||||
|
||||
if (input.userVerificationRequirement !== 'not specified') {
|
||||
authenticatorSelection.userVerification = input.userVerificationRequirement;
|
||||
isAuthenticatorSelectionSpecified = true;
|
||||
}
|
||||
|
||||
if (isAuthenticatorSelectionSpecified) {
|
||||
publicKey.authenticatorSelection = authenticatorSelection;
|
||||
}
|
||||
|
||||
if (input.createTimeout !== 0) {
|
||||
publicKey.timeout = input.createTimeout * 1000;
|
||||
}
|
||||
|
||||
const excludeCredentials = getExcludeCredentials(input.excludeCredentialIds);
|
||||
if (excludeCredentials.length > 0) {
|
||||
publicKey.excludeCredentials = excludeCredentials;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await doRegister(publicKey);
|
||||
returnSuccess(result, input.initLabel, input.initLabelPrompt);
|
||||
} catch (error) {
|
||||
returnFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
function doRegister(publicKey) {
|
||||
return navigator.credentials.create({publicKey});
|
||||
}
|
||||
|
||||
function getPubKeyCredParams(signatureAlgorithmsList) {
|
||||
const pubKeyCredParams = [];
|
||||
if (signatureAlgorithmsList.length === 0) {
|
||||
pubKeyCredParams.push({type: "public-key", alg: -7});
|
||||
return pubKeyCredParams;
|
||||
}
|
||||
|
||||
for (const entry of signatureAlgorithmsList) {
|
||||
pubKeyCredParams.push({
|
||||
type: "public-key",
|
||||
alg: entry
|
||||
});
|
||||
}
|
||||
|
||||
return pubKeyCredParams;
|
||||
}
|
||||
|
||||
function getExcludeCredentials(excludeCredentialIds) {
|
||||
const excludeCredentials = [];
|
||||
if (excludeCredentialIds === "") {
|
||||
return excludeCredentials;
|
||||
}
|
||||
|
||||
for (const entry of excludeCredentialIds.split(',')) {
|
||||
excludeCredentials.push({
|
||||
type: "public-key",
|
||||
id: base64url.parse(entry, {loose: true})
|
||||
});
|
||||
}
|
||||
|
||||
return excludeCredentials;
|
||||
}
|
||||
|
||||
function getTransportsAsString(transportsList) {
|
||||
if (!Array.isArray(transportsList)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return transportsList.join();
|
||||
}
|
||||
|
||||
function returnSuccess(result, initLabel, initLabelPrompt) {
|
||||
document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), {pad: false});
|
||||
document.getElementById("attestationObject").value = base64url.stringify(new Uint8Array(result.response.attestationObject), {pad: false});
|
||||
document.getElementById("publicKeyCredentialId").value = base64url.stringify(new Uint8Array(result.rawId), {pad: false});
|
||||
|
||||
if (typeof result.response.getTransports === "function") {
|
||||
const transports = result.response.getTransports();
|
||||
if (transports) {
|
||||
document.getElementById("transports").value = getTransportsAsString(transports);
|
||||
}
|
||||
} else {
|
||||
console.log("Your browser is not able to recognize supported transport media for the authenticator.");
|
||||
}
|
||||
|
||||
let labelResult = window.prompt(initLabelPrompt, initLabel);
|
||||
if (labelResult === null) {
|
||||
labelResult = initLabel;
|
||||
}
|
||||
document.getElementById("authenticatorLabel").value = labelResult;
|
||||
|
||||
document.getElementById("register").submit();
|
||||
}
|
||||
|
||||
function returnFailure(err) {
|
||||
document.getElementById("error").value = err;
|
||||
document.getElementById("register").submit();
|
||||
}
|
|
@ -29,6 +29,13 @@
|
|||
<script src="${url.resourcesPath}/${script}" type="text/javascript"></script>
|
||||
</#list>
|
||||
</#if>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"rfc4648": "${url.resourcesCommonPath}/node_modules/rfc4648/lib/rfc4648.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="${url.resourcesPath}/js/menu-button-links.js" type="module"></script>
|
||||
<#if scripts??>
|
||||
<#list scripts as script>
|
||||
|
|
|
@ -69,99 +69,29 @@
|
|||
</#if>
|
||||
|
||||
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
||||
<input id="authenticateWebAuthnButton" type="button" onclick="webAuthnAuthenticate()" autofocus="autofocus"
|
||||
<input id="authenticateWebAuthnButton" type="button" autofocus="autofocus"
|
||||
value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
|
||||
</div>
|
||||
</div>
|
||||
</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) {
|
||||
$("#error").val("${msg("webauthn-unsupported-browser-text")?no_esc}");
|
||||
$("#webauth").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
let challenge = "${challenge}";
|
||||
let userVerification = "${userVerification}";
|
||||
let rpId = "${rpId}";
|
||||
let publicKey = {
|
||||
rpId : rpId,
|
||||
challenge: base64url.decode(challenge, { loose: true })
|
||||
<script type="module">
|
||||
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
|
||||
const authButton = document.getElementById('authenticateWebAuthnButton');
|
||||
authButton.addEventListener("click", function() {
|
||||
const input = {
|
||||
isUserIdentified : ${isUserIdentified},
|
||||
challenge : '${challenge}',
|
||||
userVerification : '${userVerification}',
|
||||
rpId : '${rpId}',
|
||||
createTimeout : ${createTimeout},
|
||||
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
|
||||
$("#authenticatorData").val(base64url.encode(new Uint8Array(authenticatorData), { pad: false }));
|
||||
$("#signature").val(base64url.encode(new Uint8Array(signature), { pad: false }));
|
||||
$("#credentialId").val(result.id);
|
||||
if(result.response.userHandle) {
|
||||
$("#userHandle").val(base64url.encode(new Uint8Array(result.response.userHandle), { pad: false }));
|
||||
}
|
||||
$("#webauth").submit();
|
||||
})
|
||||
.catch((err) => {
|
||||
$("#error").val(err);
|
||||
$("#webauth").submit();
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
authenticateByWebAuthn(input);
|
||||
});
|
||||
</script>
|
||||
|
||||
<#elseif section = "info">
|
||||
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
|
||||
<div id="kc-registration">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<#import "password-commons.ftl" as passwordCommons>
|
||||
<#import "template.ftl" as layout>
|
||||
<#import "password-commons.ftl" as passwordCommons>
|
||||
|
||||
<@layout.registrationLayout; section>
|
||||
<@layout.registrationLayout; section>
|
||||
<#if section = "title">
|
||||
title
|
||||
<#elseif section = "header">
|
||||
|
@ -21,164 +21,34 @@
|
|||
</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) {
|
||||
$("#error").val("${msg("webauthn-unsupported-browser-text")?no_esc}");
|
||||
$("#register").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
// mandatory parameters
|
||||
let challenge = "${challenge}";
|
||||
let userid = "${userid}";
|
||||
let username = "${username}";
|
||||
|
||||
let signatureAlgorithms =[<#list signatureAlgorithms as sigAlg>${sigAlg},</#list>]
|
||||
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,
|
||||
<script type="module">
|
||||
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
|
||||
const registerButton = document.getElementById('registerWebAuthn');
|
||||
registerButton.addEventListener("click", function() {
|
||||
const input = {
|
||||
challenge : '${challenge}',
|
||||
userid : '${userid}',
|
||||
username : '${username}',
|
||||
signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg},</#list>],
|
||||
rpEntityName : '${rpEntityName}',
|
||||
rpId : '${rpId}',
|
||||
attestationConveyancePreference : '${attestationConveyancePreference}',
|
||||
authenticatorAttachment : '${authenticatorAttachment}',
|
||||
requireResidentKey : '${requireResidentKey}',
|
||||
userVerificationRequirement : '${userVerificationRequirement}',
|
||||
createTimeout : ${createTimeout},
|
||||
excludeCredentialIds : '${excludeCredentialIds}',
|
||||
initLabel : "${msg("webauthn-registration-init-label")?no_esc}",
|
||||
initLabelPrompt : "${msg("webauthn-registration-init-label-prompt")?no_esc}",
|
||||
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), {pad: false}));
|
||||
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
|
||||
$("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), {pad: false}));
|
||||
|
||||
if (typeof result.response.getTransports === "function") {
|
||||
let transports = result.response.getTransports();
|
||||
if (transports) {
|
||||
$("#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;
|
||||
$("#authenticatorLabel").val(labelResult);
|
||||
|
||||
$("#register").submit();
|
||||
|
||||
})
|
||||
.catch(function (err) {
|
||||
$("#error").val(err);
|
||||
$("#register").submit();
|
||||
|
||||
registerByWebAuthn(input);
|
||||
});
|
||||
}
|
||||
|
||||
function getPubKeyCredParams(signatureAlgorithmsList) {
|
||||
let pubKeyCredParams = [];
|
||||
if (signatureAlgorithmsList === []) {
|
||||
pubKeyCredParams.push({type: "public-key", alg: -7});
|
||||
return pubKeyCredParams;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<input type="submit"
|
||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
id="registerWebAuthn" value="${msg("doRegisterSecurityKey")}" onclick="registerSecurityKey()"/>
|
||||
id="registerWebAuthn" value="${msg("doRegisterSecurityKey")}"/>
|
||||
|
||||
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
|
||||
|
@ -191,4 +61,4 @@
|
|||
</#if>
|
||||
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
</@layout.registrationLayout>
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"jquery": "^3.7.1",
|
||||
"patternfly": "^3.59.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"rfc4648": "^1.5.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.10.0",
|
||||
"devDependencies": {
|
||||
|
|
|
@ -29,6 +29,9 @@ dependencies:
|
|||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
rfc4648:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
|
||||
devDependencies:
|
||||
'@rollup/plugin-commonjs':
|
||||
|
@ -1233,3 +1236,7 @@ packages:
|
|||
/wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: true
|
||||
|
||||
/rfc4648@1.5.3:
|
||||
resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==}
|
||||
dev: false
|
||||
|
|
Loading…
Reference in a new issue