KEYCLOAK-12177 KEYCLOAK-12178 WebAuthn: Improve usability (#6710)

This commit is contained in:
Martin Bartoš 2020-02-05 08:35:47 +01:00 committed by GitHub
parent 42fdc12bdc
commit b0c4913587
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 213 additions and 130 deletions

View file

@ -24,7 +24,7 @@ public enum LoginFormsPages {
LOGIN, LOGIN_USERNAME, LOGIN_PASSWORD, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_WEBAUTHN, LOGIN_VERIFY_EMAIL,
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM;
}

View file

@ -82,6 +82,8 @@ public interface LoginFormsProvider extends Provider {
Response createErrorPage(Response.Status status);
Response createWebAuthnErrorPage();
Response createOAuthGrant();
Response createSelectAuthenticator();

View file

@ -22,7 +22,6 @@ import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.util.exception.WebAuthnException;
import org.jboss.logging.Logger;
import org.keycloak.WebAuthnConstants;
import org.keycloak.authentication.AuthenticationFlowContext;
@ -53,6 +52,8 @@ import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
import static org.keycloak.services.messages.Messages.*;
/**
* Authenticator for WebAuthn authentication, which will be typically used when WebAuthn is used as second factor.
*/
@ -60,6 +61,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
private static final Logger logger = Logger.getLogger(WebAuthnAuthenticator.class);
private KeycloakSession session;
private WebAuthnAuthenticatorsBean authenticators;
public WebAuthnAuthenticator(KeycloakSession session) {
this.session = session;
@ -82,7 +84,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
boolean isUserIdentified = false;
if (user != null) {
// in 2 Factor Scenario where the user has already been identified
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType());
authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType());
if (authenticators.getAuthenticators().isEmpty()) {
// require the user to register webauthn authenticator
return;
@ -119,7 +121,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
// receive error from navigator.credentials.get()
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
setErrorResponse(context, ERR_WEBAUTHN_API_GET, errorMsgFromWebAuthnApi);
setErrorResponse(context, WEBAUTHN_ERROR_API_GET, errorMsgFromWebAuthnApi);
return;
}
@ -154,7 +156,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
context.getEvent()
.detail("first_authenticated_user_id", firstAuthenticatedUserId)
.detail("web_authn_authenticator_authenticated_user_id", userId);
setErrorResponse(context, ERR_DIFFERENT_USER_AUTHENTICATED, null);
setErrorResponse(context, WEBAUTHN_ERROR_DIFFERENT_USER, null);
return;
}
} else {
@ -181,7 +183,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
try {
result = session.userCredentialManager().isValid(context.getRealm(), user, cred);
} catch (WebAuthnException wae) {
setErrorResponse(context, ERR_WEBAUTHN_VERIFICATION_FAIL, wae.getMessage());
setErrorResponse(context, WEBAUTHN_ERROR_AUTH_VERIFICATION, wae.getMessage());
return;
}
String encodedCredentialID = Base64Url.encode(credentialId);
@ -197,7 +199,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
context.getEvent()
.detail("web_authn_authenticated_user_id", userId)
.detail("public_key_credential_id", encodedCredentialID);
setErrorResponse(context, ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND, null);
setErrorResponse(context, WEBAUTHN_ERROR_USER_NOT_FOUND, null);
context.cancelLogin();
}
}
@ -232,64 +234,62 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
private static final String ERR_LABEL = "web_authn_authentication_error";
private static final String ERR_DETAIL_LABEL = "web_authn_authentication_error_detail";
private static final String ERR_NO_AUTHENTICATORS_REGISTERED = "No WebAuthn Authenticator registered.";
private static final String ERR_WEBAUTHN_API_GET = "Failed to authenticate by the WebAuthn Authenticator";
private static final String ERR_DIFFERENT_USER_AUTHENTICATED = "First authenticated user is not the one authenticated by the WebAuthn authenticator.";
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WebAuthn Authentication result is invalid.";
private static final String ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthn Authenticator";
private void setErrorResponse(AuthenticationFlowContext context, final String errorCase, final String errorMessage) {
Response errorResponse = null;
switch (errorCase) {
case ERR_NO_AUTHENTICATORS_REGISTERED:
case WEBAUTHN_ERROR_REGISTRATION:
logger.warn(errorCase);
context.getEvent()
.detail(ERR_LABEL, errorCase)
.error(Errors.INVALID_USER_CREDENTIALS);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
errorResponse = createErrorResponse(context, errorCase);
context.failure(AuthenticationFlowError.INVALID_CREDENTIALS, errorResponse);
break;
case ERR_WEBAUTHN_API_GET:
case WEBAUTHN_ERROR_API_GET:
logger.warnv("error returned from navigator.credentials.get(). {0}", errorMessage);
context.getEvent()
.detail(ERR_LABEL, errorCase)
.detail(ERR_DETAIL_LABEL, errorMessage)
.error(Errors.NOT_ALLOWED);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
errorResponse = createErrorResponse(context, errorCase);
context.failure(AuthenticationFlowError.INVALID_USER, errorResponse);
break;
case ERR_DIFFERENT_USER_AUTHENTICATED:
case WEBAUTHN_ERROR_DIFFERENT_USER:
logger.warn(errorCase);
context.getEvent()
.detail(ERR_LABEL, errorCase)
.error(Errors.DIFFERENT_USER_AUTHENTICATED);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
errorResponse = createErrorResponse(context, errorCase);
context.failure(AuthenticationFlowError.USER_CONFLICT, errorResponse);
break;
case ERR_WEBAUTHN_VERIFICATION_FAIL:
case WEBAUTHN_ERROR_AUTH_VERIFICATION:
logger.warnv("WebAuthn API .get() response validation failure. {0}", errorMessage);
context.getEvent()
.detail(ERR_LABEL, errorCase)
.detail(ERR_DETAIL_LABEL, errorMessage)
.error(Errors.INVALID_USER_CREDENTIALS);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
errorResponse = createErrorResponse(context, errorCase);
context.failure(AuthenticationFlowError.INVALID_USER, errorResponse);
break;
case ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND:
case WEBAUTHN_ERROR_USER_NOT_FOUND:
logger.warn(errorCase);
context.getEvent().detail(ERR_LABEL, errorCase);
context.getEvent().error(Errors.USER_NOT_FOUND);
context.getEvent()
.detail(ERR_LABEL, errorCase)
.error(Errors.USER_NOT_FOUND);
errorResponse = createErrorResponse(context, errorCase);
context.failure(AuthenticationFlowError.UNKNOWN_USER, errorResponse);
break;
default:
// NOP
}
}
private Response createErrorResponse(AuthenticationFlowContext context, final String errorCase) {
LoginFormsProvider provider = context.form().setError(errorCase);
if (authenticators != null && authenticators.getAuthenticators() != null) {
provider.setAttribute(WebAuthnConstants.ALLOWED_AUTHENTICATORS, authenticators);
}
return provider.createWebAuthnErrorPage();
}
}

View file

@ -64,17 +64,20 @@ import com.webauthn4j.validator.attestation.statement.packed.PackedAttestationSt
import com.webauthn4j.validator.attestation.statement.tpm.TPMAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.u2f.FIDOU2FAttestationStatementValidator;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.NullCertPathTrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.ecdaa.DefaultECDAATrustworthinessValidator;
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import static org.keycloak.services.messages.Messages.*;
/**
* Required action for register WebAuthn 2-factor credential for the user
*/
public class WebAuthnRegister implements RequiredActionProvider, CredentialRegistrator {
private static final String WEB_AUTHN_TITLE_ATTR = "webAuthnTitle";
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
private KeycloakSession session;
private CertPathTrustworthinessValidator certPathtrustValidator;
@ -167,7 +170,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
// receive error from navigator.credentials.create()
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
setErrorResponse(context, ERR_WEBAUTHN_API_CREATE, errorMsgFromWebAuthnApi);
setErrorResponse(context, WEBAUTHN_ERROR_REGISTER_VERIFICATION, errorMsgFromWebAuthnApi);
return;
}
@ -218,11 +221,11 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
context.success();
} catch (WebAuthnException wae) {
if (logger.isDebugEnabled()) logger.debug(wae.getMessage(), wae);
setErrorResponse(context, ERR_WEBAUTHN_API_CREATE, wae.getMessage());
setErrorResponse(context, WEBAUTHN_ERROR_REGISTRATION, wae.getMessage());
return;
} catch (Exception e) {
if (logger.isDebugEnabled()) logger.debug(e.getMessage(), e);
setErrorResponse(context, ERR_WEBAUTHN_API_CREATE, e.getMessage());
setErrorResponse(context, WEBAUTHN_ERROR_REGISTRATION, e.getMessage());
return;
}
}
@ -326,25 +329,12 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
private static final String ERR_LABEL = "web_authn_registration_error";
private static final String ERR_DETAIL_LABEL = "web_authn_registration_error_detail";
private static final String ERR_WEBAUTHN_API_CREATE = "Failed to authenticate by the WebAuthn Authenticator";
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WetAuthn Registration result is invalid.";
private static final String ERR_REGISTRATION_FAIL = "Failed to register your WebAuthn Authenticator.";
private static final String REGISTRATION_ATTR = "webAuthnRegistration";
private void setErrorResponse(RequiredActionContext context, final String errorCase, final String errorMessage) {
Response errorResponse = null;
switch (errorCase) {
case ERR_WEBAUTHN_API_CREATE:
logger.warnv("error returned from navigator.credentials.create(). {0}", errorMessage);
context.getEvent()
.detail(ERR_LABEL, errorCase)
.detail(ERR_DETAIL_LABEL, errorMessage)
.error(Errors.NOT_ALLOWED);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
context.challenge(errorResponse);
break;
case ERR_WEBAUTHN_VERIFICATION_FAIL:
case WEBAUTHN_ERROR_REGISTER_VERIFICATION:
logger.warnv("WebAuthn API .create() response validation failure. {0}", errorMessage);
context.getEvent()
.detail(ERR_LABEL, errorCase)
@ -352,10 +342,12 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
.error(Errors.INVALID_USER_CREDENTIALS);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
.setAttribute(WEB_AUTHN_TITLE_ATTR, WEBAUTHN_REGISTER_TITLE)
.setAttribute(REGISTRATION_ATTR,true)
.createWebAuthnErrorPage();
context.challenge(errorResponse);
break;
case ERR_REGISTRATION_FAIL:
case WEBAUTHN_ERROR_REGISTRATION:
logger.warn(errorCase);
context.getEvent()
.detail(ERR_LABEL, errorCase)
@ -363,7 +355,9 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
.error(Errors.INVALID_REGISTRATION);
errorResponse = context.form()
.setError(errorCase)
.createErrorPage(Response.Status.BAD_REQUEST);
.setAttribute(WEB_AUTHN_TITLE_ATTR, WEBAUTHN_REGISTER_TITLE)
.setAttribute(REGISTRATION_ATTR,true)
.createWebAuthnErrorPage();
context.challenge(errorResponse);
break;
default:

View file

@ -547,6 +547,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return createResponse(LoginFormsPages.ERROR);
}
@Override
public Response createWebAuthnErrorPage() {
return createResponse(LoginFormsPages.ERROR_WEBAUTHN);
}
@Override
public Response createOAuthGrant() {
return createResponse(LoginFormsPages.OAUTH_GRANT);

View file

@ -58,6 +58,8 @@ public class Templates {
return "info.ftl";
case ERROR:
return "error.ftl";
case ERROR_WEBAUTHN:
return "webauthn-error.ftl";
case LOGIN_UPDATE_PROFILE:
return "login-update-profile.ftl";
case CODE:

View file

@ -240,4 +240,16 @@ public class Messages {
public static final String DELEGATION_FAILED = "delegationFailedMessage";
public static final String DELEGATION_FAILED_HEADER = "delegationFailedHeader";
// WebAuthn
public static final String WEBAUTHN_REGISTER_TITLE = "webauthn-registration-title";
public static final String WEBAUTHN_LOGIN_TITLE = "webauthn-login-title";
public static final String WEBAUTHN_ERROR_TITLE = "webauthn-error-title";
// WebAuthn Error
public static final String WEBAUTHN_ERROR_REGISTRATION = "webauthn-error-registration";
public static final String WEBAUTHN_ERROR_API_GET = "webauthn-error-api-get";
public static final String WEBAUTHN_ERROR_DIFFERENT_USER = "webauthn-error-different-user";
public static final String WEBAUTHN_ERROR_AUTH_VERIFICATION = "webauthn-error-auth-verification";
public static final String WEBAUTHN_ERROR_REGISTER_VERIFICATION = "webauthn-error-register-verification";
public static final String WEBAUTHN_ERROR_USER_NOT_FOUND = "webauthn-error-user-not-found";
}

View file

@ -0,0 +1,36 @@
package org.keycloak.testsuite.pages.webauthn;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
@FindBy(id = "kc-try-again")
private WebElement tryAgainButton;
public void clickTryAgain() {
tryAgainButton.click();
}
@Override
public boolean isCurrent() {
try {
driver.findElement(By.id("kc-try-again"));
driver.findElement(By.id("kc-error-credential-form"));
return true;
} catch (NoSuchElementException e) {
return false;
}
}
@Override
public void open() {
throw new UnsupportedOperationException();
}
}

View file

@ -18,20 +18,12 @@
package org.keycloak.testsuite.pages.webauthn;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.openqa.selenium.By;
/**
* Page shown during WebAuthn login. Page is useful with Chrome testing API
*/
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
// After click the button, the "navigator.credentials.get" will be called on the browser side, which should automatically
// login user with the chrome testing API
public void confirmWebAuthnLogin() {
driver.findElement(By.cssSelector("input[type=\"button\"]")).click();
}
public boolean isCurrent() {
return driver.getPageSource().contains("navigator.credentials.get");
}

View file

@ -168,9 +168,7 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
loginPage.open();
loginPage.login(username, password);
// Confirm login on the WebAuthn login page
webAuthnLoginPage.assertCurrent();
webAuthnLoginPage.confirmWebAuthnLogin();
// User is authenticated by Chrome WebAuthN testing API
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -249,26 +247,23 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
CredentialRepresentation webAuthnCredential1 = rep.stream()
.filter(credential -> WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(credential.getType()))
.findFirst().get();
.findFirst().orElse(null);
Assert.assertNotNull(webAuthnCredential1);
Assert.assertEquals("label1", webAuthnCredential1.getUserLabel());
CredentialRepresentation webAuthnCredential2 = rep.stream()
.filter(credential -> WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType()))
.findFirst().get();
.findFirst().orElse(null);
Assert.assertNotNull(webAuthnCredential2);
Assert.assertEquals("label2", webAuthnCredential2.getUserLabel());
// Assert user needs to authenticate first with "webauthn" during login
loginPage.open();
loginPage.login("test-user@localhost", "password");
webAuthnLoginPage.assertCurrent();
Assert.assertTrue(driver.getPageSource().contains(regPubKeyCredentialId1));
webAuthnLoginPage.confirmWebAuthnLogin();
// Assert user needs to authenticate also with "webauthn-passwordless"
webAuthnLoginPage.assertCurrent();
Assert.assertTrue(driver.getPageSource().contains(regPubKeyCredentialId2));
webAuthnLoginPage.confirmWebAuthnLogin();
// User is authenticated by Chrome WebAuthN testing API
// Assert user logged now
appPage.assertCurrent();

View file

@ -12,6 +12,7 @@ doDecline=Decline
doForgotPassword=Forgot Password?
doClickHere=Click here
doImpersonate=Impersonate
doTryAgain=Try again
doTryAnotherWay=Try Another Way
kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured
@ -346,9 +347,24 @@ auth-username-form-display-name=Username
auth-username-form-help-text=Start log in by entering your username
auth-username-password-form-display-name=Username and password
auth-username-password-form-help-text=Log in by entering your username and password.
# WebAuthn
webauthn-display-name=Security Key
webauthn-help-text=Use your security key to log in.
webauthn-passwordless-display-name=Security Key
webauthn-passwordless-help-text=Use your security key for passwordless log in.
webauthn-login-title=Security Key login
webauthn-registration-title=Security Key Registration
webauthn-available-authenticators=Available authenticators
# WebAuthn Error
webauthn-error-title=Security Key Error
webauthn-error-registration=Failed to register your Security key.
webauthn-error-api-get=Failed to authenticate by the Security key.
webauthn-error-different-user=First authenticated user is not the one authenticated by the Security key.
webauthn-error-auth-verification=Security key authentication result is invalid.
webauthn-error-register-verification=Security key registration result is invalid.
webauthn-error-user-not-found=Unknown user authenticated by the Security key.
identity-provider-redirector=Connect with another Identity Provider

View file

@ -1,4 +1,4 @@
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false displayWide=false>
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false displayWide=false showAnotherWayIfPresent=true>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" class="${properties.kcHtmlClass!}">
@ -119,7 +119,7 @@
<#nested "form">
<#if auth?has_content && auth.showTryAnotherWayLink() >
<#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" <#if displayWide>class="${properties.kcContentWrapperClass!}"</#if>>
<div <#if displayWide>class="${properties.kcFormSocialAccountContentClass!} ${properties.kcFormSocialAccountClass!}"</#if>>
<div class="${properties.kcFormGroupClass!}">

View file

@ -1,9 +1,9 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
<@layout.registrationLayout showAnotherWayIfPresent=false; section>
<#if section = "title">
title
<#elseif section = "header">
${msg("loginTitleHtml", realm.name)}
${kcSanitize(msg("webauthn-login-title"))?no_esc}
<#elseif section = "form">
<form id="webauth" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
@ -19,33 +19,9 @@
<#if authenticators??>
<form id="authn_select" class="${properties.kcFormClass!}">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Use</th>
<th>Authenticator Label</th>
</tr>
</thead>
<tbody>
<#list authenticators.authenticators as authenticator>
<tr>
<td>
<input type="checkbox" name="authn_use_chk" value="${authenticator.credentialId}" checked/>
</td>
<td>
${authenticator.label}
</td>
</tr>
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
</#list>
</tbody>
</table>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login" id="kc-login" type="button" value="${msg("doLogIn")}" onclick="checkAllowCredentials();"/>
</div>
</div>
</form>
</#if>
@ -53,36 +29,38 @@
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
<script type="text/javascript">
window.onload = function doAuhenticateAutomatically() {
window.onload = () => {
let isUserIdentified = ${isUserIdentified};
if (!isUserIdentified) doAuthenticate([]);
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 && authn_use.checked) {
if (authn_use !== undefined) {
if (authn_use.length === undefined) {
allowCredentials.push({
id: base64url.decode(authn_use.value, {loose: true}),
type: 'public-key',
})
} else if (authn_use.length != undefined) {
for (var i = 0; i < authn_use.length; i++) {
if (authn_use[i].checked) {
});
} 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) {
let challenge = "${challenge}";
let userVerification = "${userVerification}";
@ -99,7 +77,7 @@
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
navigator.credentials.get({publicKey})
.then(function(result) {
.then((result) => {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
@ -115,7 +93,7 @@
}
$("#webauth").submit();
})
.catch(function(err) {
.catch((err) => {
$("#error").val(err);
$("#webauth").submit();
})

View file

@ -0,0 +1,48 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=true; section>
<#if section = "header">
${kcSanitize(msg("webauthn-error-title"))?no_esc}
<#elseif section = "form">
<script type="text/javascript">
refreshPage = () => {
if ('${execution}' === "webauthn-register") {
location.reload();
return false;
}
document.getElementById('executionValue').value = '${execution}';
document.getElementById('kc-error-credential-form').submit();
}
</script>
<form id="kc-error-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}"
method="post">
<input type="hidden" id="executionValue" name="authenticationExecution"/>
</form>
<#if authenticators??>
<table class="table">
<thead>
<tr>
<th>${kcSanitize(msg("webauthn-available-authenticators")?no_esc)}</th>
</tr>
</thead>
<tbody>
<#list authenticators.authenticators as authenticator>
<tr>
<th>
<span id="kc-webauthn-authenticator">${kcSanitize(authenticator.label)?no_esc}</span>
</th>
</tr>
</#list>
</tbody>
</table>
</#if>
<div id="kc-error-message">
<input tabindex="4" onclick="refreshPage()" type="button"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="try-again" id="kc-try-again" value="${kcSanitize(msg("doTryAgain"))?no_esc}"/>
</div>
</#if>
</@layout.registrationLayout>

View file

@ -3,8 +3,10 @@
<#if section = "title">
title
<#elseif section = "header">
${msg("loginTitleHtml", realm.name)}
<span class="${properties.kcWebAuthnKeyIcon}"></span>
${kcSanitize(msg("webauthn-registration-title"))?no_esc}
<#elseif section = "form">
<form id="register" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>

View file

@ -36,6 +36,7 @@ kcFeedbackSuccessIcon=pficon pficon-ok
kcFeedbackInfoIcon=pficon pficon-info
kcResetFlowIcon=pficon pficon-arrow fa-2x
kcWebAuthnKeyIcon=pficon pficon-key
kcFormClass=form-horizontal
kcFormGroupClass=form-group