KEYCLOAK-12799 Missing Cancel button on The WebAuthn setup screen when using AIA

This commit is contained in:
mabartos 2020-03-03 17:02:05 +01:00 committed by Marek Posolda
parent fae333750a
commit a1bbab9eb2
8 changed files with 334 additions and 112 deletions

View file

@ -41,6 +41,8 @@ import org.keycloak.credential.WebAuthnCredentialProviderFactory;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy;
@ -135,6 +137,8 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
excludeCredentialIds = stringifyExcludeCredentialIds(webAuthnCredentialPubKeyIds);
}
String isSetRetry = context.getHttpRequest().getDecodedFormParameters().getFirst(WebAuthnConstants.IS_SET_RETRY);
Response form = context.form()
.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue)
.setAttribute(WebAuthnConstants.USER_ID, userId)
@ -148,6 +152,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
.setAttribute(WebAuthnConstants.USER_VERIFICATION_REQUIREMENT, userVerificationRequirement)
.setAttribute(WebAuthnConstants.CREATE_TIMEOUT, createTimeout)
.setAttribute(WebAuthnConstants.EXCLUDE_CREDENTIAL_IDS, excludeCredentialIds)
.setAttribute(WebAuthnConstants.IS_SET_RETRY, isSetRetry)
.createForm("webauthn-register.ftl");
context.challenge(form);
}

View file

@ -194,10 +194,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
if (status != null) {
attributes.put("statusCode", status.getStatusCode());
}
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) {
attributes.put("isAppInitiatedAction", true);
}
switch (page) {
case LOGIN_CONFIG_TOTP:
@ -447,6 +443,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
if (realm != null && user != null && session != null) {
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
}
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) {
attributes.put("isAppInitiatedAction", true);
}
}
/**

View file

@ -1,5 +1,6 @@
package org.keycloak.testsuite.pages.webauthn;
import org.junit.Assert;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
@ -13,11 +14,23 @@ public class WebAuthnErrorPage extends LanguageComboboxAwarePage {
@FindBy(id = "kc-try-again")
private WebElement tryAgainButton;
// Available only with AIA
@FindBy(id = "cancelWebAuthnAIA")
private WebElement cancelRegistrationAIA;
public void clickTryAgain() {
tryAgainButton.click();
}
public void clickCancelRegistrationAIA() {
try {
cancelRegistrationAIA.click();
} catch (NoSuchElementException e) {
Assert.fail("It only works with AIA");
}
}
@Override
public boolean isCurrent() {
try {

View file

@ -20,6 +20,9 @@ package org.keycloak.testsuite.pages.webauthn;
import org.junit.Assert;
import org.keycloak.testsuite.pages.AbstractPage;
import org.openqa.selenium.Alert;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
@ -31,6 +34,23 @@ import org.openqa.selenium.support.ui.WebDriverWait;
*/
public class WebAuthnRegisterPage extends AbstractPage {
// Available only with AIA
@FindBy(id = "registerWebAuthnAIA")
private WebElement registerAIAButton;
// Available only with AIA
@FindBy(id = "cancelWebAuthnAIA")
private WebElement cancelAIAButton;
public void confirmAIA() {
Assert.assertTrue("It only works with AIA", isAIA());
registerAIAButton.click();
}
public void cancelAIA() {
Assert.assertTrue("It only works with AIA", isAIA());
cancelAIAButton.click();
}
public void registerWebAuthnCredential(String authenticatorLabel) {
// label edit after registering authenicator by .create()
@ -43,8 +63,20 @@ public class WebAuthnRegisterPage extends AbstractPage {
promptDialog.accept();
}
private boolean isAIA() {
try {
registerAIAButton.getText();
cancelAIAButton.getText();
return true;
} catch (NoSuchElementException e) {
return false;
}
}
public boolean isCurrent() {
if (isAIA()) {
return true;
}
// Cant verify the page in case that prompt is shown. Prompt is shown immediately when WebAuthnRegisterPage is displayed
throw new UnsupportedOperationException();
}

View file

@ -0,0 +1,131 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.webauthn;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.events.Details;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.testsuite.WebAuthnAssume;
import org.keycloak.testsuite.actions.AbstractAppInitiatedActionTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
import org.keycloak.testsuite.pages.PasswordPage;
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
import org.keycloak.testsuite.util.FlowUtil;
import java.util.ArrayList;
import java.util.List;
import static org.keycloak.common.Profile.Feature.WEB_AUTHN;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.ALTERNATIVE;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
@EnableFeature(value = WEB_AUTHN, skipRestart = true, onlyForProduct = true)
@AuthServerContainerExclude(REMOTE)
public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTest {
@Page
LoginUsernameOnlyPage usernamePage;
@Page
PasswordPage passwordPage;
@Page
WebAuthnRegisterPage registerPage;
public AppInitiatedActionWebAuthnTest() {
super(WebAuthnRegisterFactory.PROVIDER_ID);
}
@Override
protected boolean isImportAfterEachMethod() {
return true;
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
action.setAlias(WebAuthnRegisterFactory.PROVIDER_ID);
action.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID);
action.setEnabled(true);
action.setDefaultAction(true);
action.setPriority(10);
List<RequiredActionProviderRepresentation> actions = new ArrayList<>();
actions.add(action);
testRealm.setRequiredActions(actions);
}
@Before
public void setUpWebAuthnFlow() {
final String newFlowAlias = "browserWebAuthnAIA";
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> {
FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
.clear()
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
.addSubFlowExecution(REQUIRED, subFlow -> subFlow
.addAuthenticatorExecution(ALTERNATIVE, PasswordFormFactory.PROVIDER_ID)
.addAuthenticatorExecution(ALTERNATIVE, WebAuthnAuthenticatorFactory.PROVIDER_ID)))
.defineAsBrowserFlow();
});
}
@Before
public void verifyEnvironment() {
WebAuthnAssume.assumeChrome();
}
@Test
public void cancelSetupWebAuthn() {
loginUser();
doAIA();
registerPage.assertCurrent();
registerPage.cancelAIA();
assertKcActionStatus("cancelled");
}
private void loginUser() {
usernamePage.open();
usernamePage.assertCurrent();
usernamePage.login("test-user@localhost");
passwordPage.assertCurrent();
passwordPage.login("password");
events.expectLogin()
.detail(Details.USERNAME, "test-user@localhost")
.assertEvent();
}
}

View file

@ -37,10 +37,19 @@
</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>
<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}"
/>
<#if isAppInitiatedAction??>
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form" method="post">
<button type="submit"
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
id="cancelWebAuthnAIA" name="cancel-aia" value="true"/>${msg("doCancel")}
</button>
</form>
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -7,132 +7,160 @@
${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"/>
<input type="hidden" id="attestationObject" name="attestationObject"/>
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
<input type="hidden" id="error" name="error"/>
</div>
</form>
<script type="text/javascript" src="${url.resourcesPath}/node_modules/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
<script type="text/javascript">
// mandatory parameters
let challenge = "${challenge}";
let userid = "${userid}";
let username = "${username}";
<form id="register" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
<input type="hidden" id="attestationObject" name="attestationObject"/>
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
<input type="hidden" id="error" name="error"/>
</div>
</form>
let signatureAlgorithms = "${signatureAlgorithms}";
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
<script type="text/javascript" src="${url.resourcesPath}/node_modules/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
<script type="text/javascript">
let rpEntityName = "${rpEntityName}";
let rp = {name: rpEntityName};
function registerSecurityKey() {
// mandatory parameters
let challenge = "${challenge}";
let userid = "${userid}";
let username = "${username}";
let publicKey = {
challenge: base64url.decode(challenge, { loose: true }),
rp: rp,
user: {
id: base64url.decode(userid, { loose: true }),
name: username,
displayName: username
},
pubKeyCredParams: pubKeyCredParams,
}
let signatureAlgorithms = "${signatureAlgorithms}";
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
// optional parameters
let rpId = "${rpId}";
publicKey.rp.id = rpId;
let rpEntityName = "${rpEntityName}";
let rp = {name: rpEntityName};
let attestationConveyancePreference = "${attestationConveyancePreference}";
if(attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
let publicKey = {
challenge: base64url.decode(challenge, {loose: true}),
rp: rp,
user: {
id: base64url.decode(userid, {loose: true}),
name: username,
displayName: username
},
pubKeyCredParams: pubKeyCredParams,
};
let authenticatorSelection = {};
let isAuthenticatorSelectionSpecified = false;
// optional parameters
let rpId = "${rpId}";
publicKey.rp.id = rpId;
let authenticatorAttachment = "${authenticatorAttachment}";
if(authenticatorAttachment !== 'not specified') {
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
isAuthenticatorSelectionSpecified = true;
}
let attestationConveyancePreference = "${attestationConveyancePreference}";
if (attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
let requireResidentKey = "${requireResidentKey}";
if(requireResidentKey !== 'not specified') {
if(requireResidentKey === 'Yes')
authenticatorSelection.requireResidentKey = true;
else
authenticatorSelection.requireResidentKey = false;
isAuthenticatorSelectionSpecified = true;
}
let authenticatorSelection = {};
let isAuthenticatorSelectionSpecified = false;
let userVerificationRequirement = "${userVerificationRequirement}";
if(userVerificationRequirement !== 'not specified') {
authenticatorSelection.userVerification = userVerificationRequirement;
isAuthenticatorSelectionSpecified = true;
}
let authenticatorAttachment = "${authenticatorAttachment}";
if (authenticatorAttachment !== 'not specified') {
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
isAuthenticatorSelectionSpecified = true;
}
if(isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
let requireResidentKey = "${requireResidentKey}";
if (requireResidentKey !== 'not specified') {
if (requireResidentKey === 'Yes')
authenticatorSelection.requireResidentKey = true;
else
authenticatorSelection.requireResidentKey = false;
isAuthenticatorSelectionSpecified = true;
}
let createTimeout = ${createTimeout};
if(createTimeout != 0) publicKey.timeout = createTimeout * 1000;
let userVerificationRequirement = "${userVerificationRequirement}";
if (userVerificationRequirement !== 'not specified') {
authenticatorSelection.userVerification = userVerificationRequirement;
isAuthenticatorSelectionSpecified = true;
}
let excludeCredentialIds = "${excludeCredentialIds}";
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
if (isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
navigator.credentials.create({publicKey})
.then(function(result) {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
let attestationObject = result.response.attestationObject;
let publicKeyCredentialId = result.rawId;
let createTimeout = ${createTimeout};
if (createTimeout != 0) publicKey.timeout = createTimeout * 1000;
$("#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 }));
let excludeCredentialIds = "${excludeCredentialIds}";
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
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);
navigator.credentials.create({publicKey})
.then(function (result) {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
let attestationObject = result.response.attestationObject;
let publicKeyCredentialId = result.rawId;
$("#register").submit();
$("#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}));
})
.catch(function(err) {
$("#error").val(err);
$("#register").submit();
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();
function getPubKeyCredParams(signatureAlgorithms) {
let pubKeyCredParams = [];
if(signatureAlgorithms === "") {
pubKeyCredParams.push({type: "public-key", alg: -7});
})
.catch(function (err) {
$("#error").val(err);
$("#register").submit();
});
}
function getPubKeyCredParams(signatureAlgorithms) {
let pubKeyCredParams = [];
if (signatureAlgorithms === "") {
pubKeyCredParams.push({type: "public-key", alg: -7});
return pubKeyCredParams;
}
let signatureAlgorithmsList = signatureAlgorithms.split(',');
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
pubKeyCredParams.push({
type: "public-key",
alg: signatureAlgorithmsList[i]
});
}
return pubKeyCredParams;
}
let signatureAlgorithmsList = signatureAlgorithms.split(',');
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
pubKeyCredParams.push({type: "public-key", alg: signatureAlgorithmsList[i]});
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;
}
return pubKeyCredParams;
}
</script>
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;
}
</script>
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
<input type="submit"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
id="registerWebAuthnAIA" value="${msg("doRegister")}" onclick="registerSecurityKey()"
/>
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
method="post">
<button type="submit"
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
id="cancelWebAuthnAIA" name="cancel-aia" value="true"/>${msg("doCancel")}
</button>
</form>
<#else>
<script>
registerSecurityKey();
</script>
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -127,6 +127,10 @@ div.kc-logo-text span {
text-align: center;
}
#kc-webauthn-settings-form{
padding-top:8px;
}
/* #kc-content-wrapper {
overflow-y: hidden;
} */