Add test scenarios for Passwordless Webauthn AIA

Closes #9795
This commit is contained in:
Martin Bartoš 2022-01-26 14:00:02 +01:00 committed by Pavel Drozd
parent 24d6f75d11
commit 2919342f3a
6 changed files with 102 additions and 44 deletions

View file

@ -50,7 +50,10 @@ import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
* @author Stan Silvert
*/
public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKeycloakTest {
protected static final String SUCCESS = "success";
protected static final String CANCELLED = "cancelled";
@Page
protected LoginPage loginPage;
@ -59,16 +62,12 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
@Rule
public AssertEvents events = new AssertEvents(this);
protected final String aiaAction;
public AbstractAppInitiatedActionTest(String aiaAction) {
this.aiaAction = aiaAction;
}
protected abstract String getAiaAction();
protected void doAIA() {
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
String uri = builder.queryParam(KC_ACTION, this.aiaAction)
String uri = builder.queryParam(KC_ACTION, getAiaAction())
.queryParam(RESPONSE_TYPE, "code")
.queryParam(CLIENT_ID, "test-app")
.queryParam(SCOPE, "openid")

View file

@ -46,10 +46,11 @@ import static org.junit.Assert.assertTrue;
*/
public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionResetPasswordTest() {
super(UserModel.RequiredAction.UPDATE_PASSWORD.name());
@Override
protected String getAiaAction() {
return UserModel.RequiredAction.UPDATE_PASSWORD.name();
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setResetPasswordAllowed(Boolean.TRUE);
@ -86,7 +87,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
EventRepresentation loginEvent = events.expectLogin().assertEvent();
@ -121,7 +122,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
changePasswordPage.changePassword("new-password", "new-password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
}
@Test
@ -133,7 +134,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
changePasswordPage.assertCurrent();
changePasswordPage.cancel();
assertKcActionStatus("cancelled");
assertKcActionStatus(CANCELLED);
}
@Test
@ -157,7 +158,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
}
@Test
@ -184,7 +185,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
assertTrue("Logout sessions is checked by default", changePasswordPage.isLogoutSessionsChecked());
changePasswordPage.changePassword("All Right Then, Keep Your Secrets", "All Right Then, Keep Your Secrets");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
sessions = testUser.getUserSessions();
assertEquals(1, sessions.size());
@ -212,7 +213,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
changePasswordPage.uncheckLogoutSessions();
changePasswordPage.changePassword("All Right Then, Keep Your Secrets", "All Right Then, Keep Your Secrets");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
assertEquals(2, testUser.getUserSessions().size());
}

View file

@ -55,10 +55,11 @@ import static org.junit.Assert.assertTrue;
*/
public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionTotpSetupTest() {
super(UserModel.RequiredAction.CONFIGURE_TOTP.name());
@Override
public String getAiaAction() {
return UserModel.RequiredAction.CONFIGURE_TOTP.name();
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setResetPasswordAllowed(Boolean.TRUE);
@ -117,7 +118,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
.getDetails().get(Details.CODE_ID);
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent();
}
@ -352,7 +353,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
.getDetails().get(Details.CODE_ID);
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
@ -387,7 +388,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
totpPage.configure(totp.generateTOTP(totpCode));
// After totp config, user should be on the app page
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
events.poll();
events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
@ -459,7 +460,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
.getDetails().get(Details.CODE_ID);
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
@ -512,7 +513,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
.getDetails().get(Details.CODE_ID);
//RequestType reqType = appPage.getRequestType();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
oauth.openLogout();

View file

@ -37,8 +37,9 @@ import org.keycloak.testsuite.util.UserBuilder;
*/
public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedActionTest {
public AppInitiatedActionUpdateProfileTest() {
super(UserModel.RequiredAction.UPDATE_PROFILE.name());
@Override
public String getAiaAction() {
return UserModel.RequiredAction.UPDATE_PROFILE.name();
}
@Page
@ -46,7 +47,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
@Page
protected ErrorPage errorPage;
protected boolean isDynamicForm() {
return false;
}
@ -92,7 +93,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
.assertEvent();
events.expectLogin().assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
// assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -123,7 +124,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
.assertEvent();
events.expectLogin().assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
// assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@ -142,7 +143,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
updateProfilePage.assertCurrent();
updateProfilePage.cancel();
assertKcActionStatus("cancelled");
assertKcActionStatus(CANCELLED);
// assert nothing was updated in persistent store
@ -177,7 +178,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
.removeDetail(Details.CONSENT)
.assertEvent();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).assertEvent();

View file

@ -0,0 +1,28 @@
/*
* Copyright 2021 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;
/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
*/
public class AppInitiatedActionPwdLessTest extends AppInitiatedActionWebAuthnTest {
protected boolean isPasswordless() {
return true;
}
}

View file

@ -20,14 +20,18 @@ import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.UserResource;
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.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
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.actions.AbstractAppInitiatedActionTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
@ -40,7 +44,11 @@ import org.keycloak.testsuite.webauthn.pages.WebAuthnRegisterPage;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
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;
@ -56,6 +64,10 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
private VirtualAuthenticatorManager virtualManager;
protected final String WEB_AUTHN_REGISTER_PROVIDER = isPasswordless() ? WebAuthnPasswordlessRegisterFactory.PROVIDER_ID : WebAuthnRegisterFactory.PROVIDER_ID;
protected final String DEFAULT_USERNAME = "test-user@localhost";
protected final String DEFAULT_PASSWORD = "password";
@Page
LoginUsernameOnlyPage usernamePage;
@ -77,8 +89,9 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
virtualManager.removeAuthenticator();
}
public AppInitiatedActionWebAuthnTest() {
super(WebAuthnRegisterFactory.PROVIDER_ID);
@Override
public String getAiaAction() {
return WEB_AUTHN_REGISTER_PROVIDER;
}
@Override
@ -86,11 +99,15 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
return true;
}
protected boolean isPasswordless() {
return false;
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
action.setAlias(WebAuthnRegisterFactory.PROVIDER_ID);
action.setProviderId(WebAuthnRegisterFactory.PROVIDER_ID);
action.setAlias(WEB_AUTHN_REGISTER_PROVIDER);
action.setProviderId(WEB_AUTHN_REGISTER_PROVIDER);
action.setEnabled(true);
action.setDefaultAction(true);
action.setPriority(10);
@ -103,8 +120,10 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
@Before
public void setUpWebAuthnFlow() {
final String newFlowAlias = "browserWebAuthnAIA";
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server("test").run(session -> {
final String webAuthnAuthProvider = isPasswordless() ? WebAuthnPasswordlessAuthenticatorFactory.PROVIDER_ID : WebAuthnAuthenticatorFactory.PROVIDER_ID;
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server(TEST_REALM_NAME).run(session -> {
FlowUtil.inCurrentRealm(session)
.selectFlow(newFlowAlias)
.inForms(forms -> forms
@ -112,7 +131,7 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
.addAuthenticatorExecution(REQUIRED, UsernameFormFactory.PROVIDER_ID)
.addSubFlowExecution(REQUIRED, subFlow -> subFlow
.addAuthenticatorExecution(ALTERNATIVE, PasswordFormFactory.PROVIDER_ID)
.addAuthenticatorExecution(ALTERNATIVE, WebAuthnAuthenticatorFactory.PROVIDER_ID)))
.addAuthenticatorExecution(ALTERNATIVE, webAuthnAuthProvider)))
.defineAsBrowserFlow();
});
}
@ -128,7 +147,7 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
waitForPageToLoad();
assertKcActionStatus("cancelled");
assertKcActionStatus(CANCELLED);
}
@Test
@ -137,25 +156,34 @@ public class AppInitiatedActionWebAuthnTest extends AbstractAppInitiatedActionTe
doAIA();
final Supplier<Integer> getCredentialCount = () -> Optional.ofNullable(ApiUtil.findUserByUsernameId(testRealm(), DEFAULT_USERNAME))
.map(UserResource::credentials)
.map(List::size)
.orElse(0);
final int credentialsCount = getCredentialCount.get();
webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister();
webAuthnRegisterPage.registerWebAuthnCredential("authenticator1");
waitForPageToLoad();
assertKcActionStatus("success");
assertKcActionStatus(SUCCESS);
assertThat(getCredentialCount.get(), is(credentialsCount + 1));
}
private void loginUser() {
usernamePage.open();
usernamePage.assertCurrent();
usernamePage.login("test-user@localhost");
usernamePage.login(DEFAULT_USERNAME);
passwordPage.assertCurrent();
passwordPage.login("password");
passwordPage.login(DEFAULT_PASSWORD);
events.expectLogin()
.detail(Details.USERNAME, "test-user@localhost")
.detail(Details.USERNAME, DEFAULT_USERNAME)
.assertEvent();
}
}