KEYCLOAK-12177 KEYCLOAK-12178 WebAuthn: Improve usability (#6710)
This commit is contained in:
parent
42fdc12bdc
commit
b0c4913587
16 changed files with 213 additions and 130 deletions
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -82,6 +82,8 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
Response createErrorPage(Response.Status status);
|
||||
|
||||
Response createWebAuthnErrorPage();
|
||||
|
||||
Response createOAuthGrant();
|
||||
|
||||
Response createSelectAuthenticator();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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!}">
|
||||
|
|
|
@ -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>
|
||||
</#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>
|
||||
<#list authenticators.authenticators as authenticator>
|
||||
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
|
||||
</#list>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
|
@ -53,35 +29,37 @@
|
|||
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
window.onload = function doAuhenticateAutomatically() {
|
||||
let isUserIdentified = ${isUserIdentified};
|
||||
if (!isUserIdentified) doAuthenticate([]);
|
||||
}
|
||||
window.onload = () => {
|
||||
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) {
|
||||
function checkAllowCredentials() {
|
||||
let allowCredentials = [];
|
||||
let authn_use = document.forms['authn_select'].authn_use_chk;
|
||||
|
||||
if (authn_use.length === undefined && authn_use.checked) {
|
||||
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) {
|
||||
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 }),
|
||||
id: base64url.decode(authn_use[i].value, {loose: true}),
|
||||
type: 'public-key',
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doAuthenticate(allowCredentials);
|
||||
}
|
||||
doAuthenticate(allowCredentials);
|
||||
}
|
||||
|
||||
|
||||
function doAuthenticate(allowCredentials) {
|
||||
let challenge = "${challenge}";
|
||||
|
@ -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();
|
||||
})
|
||||
|
|
|
@ -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>
|
|
@ -1,10 +1,12 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout; section>
|
||||
<#if section = "title">
|
||||
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"/>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue