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.jboss.arquillian.graphene.page.Page;
|
||||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
import org.openqa.selenium.support.FindBy;
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
try {
|
try {
|
||||||
authenticateButton.getText();
|
authenticateButton.getText();
|
||||||
return driver.getPageSource().contains("navigator.credentials.get");
|
return driver.findElement(By.id("authenticateWebAuthnButton")).isDisplayed();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.hamcrest.CoreMatchers;
|
||||||
import org.keycloak.testsuite.pages.LogoutSessionsPage;
|
import org.keycloak.testsuite.pages.LogoutSessionsPage;
|
||||||
import org.keycloak.testsuite.util.WaitUtils;
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.openqa.selenium.Alert;
|
import org.openqa.selenium.Alert;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.NoSuchElementException;
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
import org.openqa.selenium.TimeoutException;
|
import org.openqa.selenium.TimeoutException;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
|
@ -85,7 +86,7 @@ public class WebAuthnRegisterPage extends LogoutSessionsPage {
|
||||||
// label edit after registering authenticator by .create()
|
// label edit after registering authenticator by .create()
|
||||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(seconds));
|
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(seconds));
|
||||||
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
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;
|
return true;
|
||||||
} catch (TimeoutException e) {
|
} catch (TimeoutException e) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -115,7 +116,7 @@ public class WebAuthnRegisterPage extends LogoutSessionsPage {
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
final String formTitle = getFormTitle();
|
final String formTitle = getFormTitle();
|
||||||
return formTitle != null && formTitle.equals("Passkey Registration") &&
|
return formTitle != null && formTitle.equals("Passkey Registration") &&
|
||||||
driver.getPageSource().contains("navigator.credentials.create");
|
driver.findElement(By.id("registerWebAuthn")).isDisplayed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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-unsupported-browser-text=WebAuthn is not supported by this browser. Try another one or contact your administrator.
|
||||||
webauthn-doAuthenticate=Sign in with Passkey
|
webauthn-doAuthenticate=Sign in with Passkey
|
||||||
webauthn-createdAt-label=Created
|
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
|
||||||
webauthn-error-title=Passkey 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>
|
<script src="${url.resourcesPath}/${script}" type="text/javascript"></script>
|
||||||
</#list>
|
</#list>
|
||||||
</#if>
|
</#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>
|
<script src="${url.resourcesPath}/js/menu-button-links.js" type="module"></script>
|
||||||
<#if scripts??>
|
<#if scripts??>
|
||||||
<#list scripts as script>
|
<#list scripts as script>
|
||||||
|
|
|
@ -69,99 +69,29 @@
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
<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"))}"
|
value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
|
||||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
|
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
<script type="module">
|
||||||
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
|
||||||
<script type="text/javascript">
|
const authButton = document.getElementById('authenticateWebAuthnButton');
|
||||||
function webAuthnAuthenticate() {
|
authButton.addEventListener("click", function() {
|
||||||
let isUserIdentified = ${isUserIdentified};
|
const input = {
|
||||||
if (!isUserIdentified) {
|
isUserIdentified : ${isUserIdentified},
|
||||||
doAuthenticate([]);
|
challenge : '${challenge}',
|
||||||
return;
|
userVerification : '${userVerification}',
|
||||||
}
|
rpId : '${rpId}',
|
||||||
checkAllowCredentials();
|
createTimeout : ${createTimeout},
|
||||||
}
|
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
|
||||||
|
};
|
||||||
function checkAllowCredentials() {
|
authenticateByWebAuthn(input);
|
||||||
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 })
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
})
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<#elseif section = "info">
|
<#elseif section = "info">
|
||||||
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
|
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
|
||||||
<div id="kc-registration">
|
<div id="kc-registration">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<#import "template.ftl" as layout>
|
<#import "template.ftl" as layout>
|
||||||
<#import "password-commons.ftl" as passwordCommons>
|
<#import "password-commons.ftl" as passwordCommons>
|
||||||
|
|
||||||
<@layout.registrationLayout; section>
|
<@layout.registrationLayout; section>
|
||||||
<#if section = "title">
|
<#if section = "title">
|
||||||
title
|
title
|
||||||
<#elseif section = "header">
|
<#elseif section = "header">
|
||||||
|
@ -21,164 +21,34 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script>
|
<script type="module">
|
||||||
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
|
||||||
<script type="text/javascript">
|
const registerButton = document.getElementById('registerWebAuthn');
|
||||||
|
registerButton.addEventListener("click", function() {
|
||||||
function registerSecurityKey() {
|
const input = {
|
||||||
|
challenge : '${challenge}',
|
||||||
// Check if WebAuthn is supported by this browser
|
userid : '${userid}',
|
||||||
if (!window.PublicKeyCredential) {
|
username : '${username}',
|
||||||
$("#error").val("${msg("webauthn-unsupported-browser-text")?no_esc}");
|
signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg},</#list>],
|
||||||
$("#register").submit();
|
rpEntityName : '${rpEntityName}',
|
||||||
return;
|
rpId : '${rpId}',
|
||||||
}
|
attestationConveyancePreference : '${attestationConveyancePreference}',
|
||||||
|
authenticatorAttachment : '${authenticatorAttachment}',
|
||||||
// mandatory parameters
|
requireResidentKey : '${requireResidentKey}',
|
||||||
let challenge = "${challenge}";
|
userVerificationRequirement : '${userVerificationRequirement}',
|
||||||
let userid = "${userid}";
|
createTimeout : ${createTimeout},
|
||||||
let username = "${username}";
|
excludeCredentialIds : '${excludeCredentialIds}',
|
||||||
|
initLabel : "${msg("webauthn-registration-init-label")?no_esc}",
|
||||||
let signatureAlgorithms =[<#list signatureAlgorithms as sigAlg>${sigAlg},</#list>]
|
initLabelPrompt : "${msg("webauthn-registration-init-label-prompt")?no_esc}",
|
||||||
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
|
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
registerByWebAuthn(input);
|
||||||
// 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();
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
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>
|
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
||||||
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
|
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
|
||||||
|
@ -191,4 +61,4 @@
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
</#if>
|
</#if>
|
||||||
</@layout.registrationLayout>
|
</@layout.registrationLayout>
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"patternfly": "^3.59.5",
|
"patternfly": "^3.59.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"rfc4648": "^1.5.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.10.0",
|
"packageManager": "pnpm@8.10.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -29,6 +29,9 @@ dependencies:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
rfc4648:
|
||||||
|
specifier: ^1.5.3
|
||||||
|
version: 1.5.3
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@rollup/plugin-commonjs':
|
'@rollup/plugin-commonjs':
|
||||||
|
@ -1233,3 +1236,7 @@ packages:
|
||||||
/wrappy@1.0.2:
|
/wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/rfc4648@1.5.3:
|
||||||
|
resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==}
|
||||||
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue