diff --git a/common/src/web/fedcm/accounts.json b/common/src/web/fedcm/accounts.json new file mode 100644 index 0000000000000..beea4d1887975 --- /dev/null +++ b/common/src/web/fedcm/accounts.json @@ -0,0 +1,17 @@ +{ + "accounts": [{ + "id": "1234", + "given_name": "John", + "name": "John Doe", + "email": "john_doe@idp.example", + "picture": "https://idp.example/profile/123", + "approved_clients": ["123", "456", "789"] + }, { + "id": "5678", + "given_name": "Aisha", + "name": "Aisha Ahmad", + "email": "aisha@idp.example", + "picture": "https://idp.example/profile/567", + "approved_clients": [] + }] +} diff --git a/common/src/web/fedcm/client_metadata.json b/common/src/web/fedcm/client_metadata.json new file mode 100644 index 0000000000000..ddde867a9f7bf --- /dev/null +++ b/common/src/web/fedcm/client_metadata.json @@ -0,0 +1,4 @@ +{ + "privacy_policy_url": "https://rp.example/privacy_policy.html", + "terms_of_service_url": "https://rp.example/terms_of_service.html" +} diff --git a/common/src/web/fedcm/fedcm.html b/common/src/web/fedcm/fedcm.html new file mode 100644 index 0000000000000..186d9311c70b8 --- /dev/null +++ b/common/src/web/fedcm/fedcm.html @@ -0,0 +1,20 @@ + + diff --git a/common/src/web/fedcm/fedcm.json b/common/src/web/fedcm/fedcm.json new file mode 100644 index 0000000000000..2512240f1e942 --- /dev/null +++ b/common/src/web/fedcm/fedcm.json @@ -0,0 +1,6 @@ +{ + "accounts_endpoint": "accounts.json", + "client_metadata_endpoint": "client_metadata.json", + "id_assertion_endpoint": "id_assertion", + "signin_url": "/signin" +} diff --git a/java/src/org/openqa/selenium/BUILD.bazel b/java/src/org/openqa/selenium/BUILD.bazel index d94215af87b8e..1e494729adfe6 100644 --- a/java/src/org/openqa/selenium/BUILD.bazel +++ b/java/src/org/openqa/selenium/BUILD.bazel @@ -12,6 +12,7 @@ java_export( name = "core", srcs = glob([ "*.java", + "federatedcredentialmanagement/*.java", "html5/*.java", "internal/*.java", "interactions/**/*.java", diff --git a/java/src/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementAccount.java b/java/src/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementAccount.java new file mode 100644 index 0000000000000..6a46e385ce271 --- /dev/null +++ b/java/src/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementAccount.java @@ -0,0 +1,101 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.federatedcredentialmanagement; + +import java.util.Map; + +/** + * Represents an account displayed in a FedCM account list. + * + * @see + * https://fedidcg.github.io/FedCM/#dictdef-identityprovideraccount + * @see + * https://fedidcg.github.io/FedCM/#webdriver-accountlist + */ +public class FederatedCredentialManagementAccount { + private final String accountId; + private final String email; + private final String name; + private final String givenName; + private final String pictureUrl; + /** + * The config URL of the identity provider that provided this account. + * + * This allows identifying the IDP in multi-IDP cases. + */ + private final String idpConfigUrl; + /** + * The login state for this account. + * + * One of LOGIN_STATE_SIGNIN and LOGIN_STATE_SIGNUP. + */ + private final String loginState; + private final String termsOfServiceUrl; + private final String privacyPolicyUrl; + + public static final String LOGIN_STATE_SIGNIN = "SignIn"; + public static final String LOGIN_STATE_SIGNUP = "SignUp"; + + public FederatedCredentialManagementAccount(Map dict) { + accountId = (String) dict.getOrDefault("accountId", null); + email = (String) dict.getOrDefault("email", null); + name = (String) dict.getOrDefault("name", null); + givenName = (String) dict.getOrDefault("givenName", null); + pictureUrl = (String) dict.getOrDefault("pictureUrl", null); + idpConfigUrl = (String) dict.getOrDefault("idpConfigUrl", null); + loginState = (String) dict.getOrDefault("loginState", null); + termsOfServiceUrl = (String) dict.getOrDefault("termsOfServiceUrl", null); + privacyPolicyUrl = (String) dict.getOrDefault("privacyPolicyUrl", null); + } + + public String getAccountid() { + return accountId; + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getGivenName() { + return givenName; + } + + public String getPictureUrl() { + return pictureUrl; + } + + public String getIdpConfigUrl() { + return idpConfigUrl; + } + + public String getLoginState() { + return loginState; + } + + public String getTermsOfServiceUrl() { + return termsOfServiceUrl; + } + + public String getPrivacyPolicyUrl() { + return privacyPolicyUrl; + } +}; diff --git a/java/src/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementDialog.java b/java/src/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementDialog.java new file mode 100644 index 0000000000000..57abcc9d6328f --- /dev/null +++ b/java/src/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementDialog.java @@ -0,0 +1,69 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.federatedcredentialmanagement; + +import java.util.List; + +/** + * Represents an open dialog of the Federated Credential Management API. + * + * @see https://fedidcg.github.io/FedCM/ + */ +public interface FederatedCredentialManagementDialog { + + String DIALOG_TYPE_ACCOUNT_LIST = "AccountChooser"; + String DIALOG_TYPE_AUTO_REAUTH = "AutoReauthn"; + + /** + * Closes the dialog as if the user had clicked X. + */ + void cancelDialog(); + + /** + * Selects an account as if the user had clicked on it. + * + * @param index The index of the account to select from the list + * returned by getAccounts(). + */ + void selectAccount(int index); + + /** + * Returns the type of the open dialog. + * + * One of DIALOG_TYPE_ACCOUNT_LIST and DIALOG_TYPE_AUTO_REAUTH. + */ + String getDialogType(); + + /** + * Returns the title of the dialog. + */ + String getTitle(); + + /** + * Returns the subtitle of the dialog or null if none. + */ + String getSubtitle(); + + /** + * Returns the accounts shown in the account chooser. + * + * If this is an auto reauth dialog, returns the single account + * that is being signed in. + */ + List getAccounts(); +} diff --git a/java/src/org/openqa/selenium/federatedcredentialmanagement/HasFederatedCredentialManagement.java b/java/src/org/openqa/selenium/federatedcredentialmanagement/HasFederatedCredentialManagement.java new file mode 100644 index 0000000000000..2d20cc1ec204c --- /dev/null +++ b/java/src/org/openqa/selenium/federatedcredentialmanagement/HasFederatedCredentialManagement.java @@ -0,0 +1,55 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.federatedcredentialmanagement; + +import org.openqa.selenium.Beta; + +/** + * Used by classes to indicate that they can interact with FedCM dialogs. + */ +@Beta +public interface HasFederatedCredentialManagement { + /** + * Disables the promise rejection delay. + * + * FedCM by default delays promise resolution in failure cases for privacy + * reasons (https://fedidcg.github.io/FedCM/#ref-for-setdelayenabled); + * this function allows turning it off to let tests run faster where this + * is not relevant. + */ + void setDelayEnabled(boolean enabled); + + /** + * Resets the FedCM dialog cooldown. + * + * If a user agent triggers a cooldown when the account chooser is dismissed, + * this function resets that cooldown so that the dialog can be triggered + * again immediately. + */ + void resetCooldown(); + + /** + * Gets the currently open FedCM dialog, or null if there is no dialog. + * + * Can be used with WebDriverWait like: + * wait.until(driver -> ((HasFederatedCredentialManagement) driver). + * getFederatedCredentialManagementDialog() != null); + */ + FederatedCredentialManagementDialog getFederatedCredentialManagementDialog(); +} + diff --git a/java/src/org/openqa/selenium/grid/web/ResourceHandler.java b/java/src/org/openqa/selenium/grid/web/ResourceHandler.java index b514a49c26b08..ac55ddc2391ad 100644 --- a/java/src/org/openqa/selenium/grid/web/ResourceHandler.java +++ b/java/src/org/openqa/selenium/grid/web/ResourceHandler.java @@ -22,6 +22,7 @@ import static com.google.common.net.MediaType.GIF; import static com.google.common.net.MediaType.HTML_UTF_8; import static com.google.common.net.MediaType.JAVASCRIPT_UTF_8; +import static com.google.common.net.MediaType.JSON_UTF_8; import static com.google.common.net.MediaType.JPEG; import static com.google.common.net.MediaType.OCTET_STREAM; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; @@ -160,6 +161,10 @@ private String mediaType(String uri) { type = JAVASCRIPT_UTF_8; break; + case "json": + type = JSON_UTF_8; + break; + case "md": case "txt": type = PLAIN_TEXT_UTF_8; diff --git a/java/src/org/openqa/selenium/remote/DriverCommand.java b/java/src/org/openqa/selenium/remote/DriverCommand.java index 993129d30bea7..3d6c90ab8ae5c 100644 --- a/java/src/org/openqa/selenium/remote/DriverCommand.java +++ b/java/src/org/openqa/selenium/remote/DriverCommand.java @@ -162,6 +162,15 @@ public interface DriverCommand { String REMOVE_CREDENTIAL = "removeCredential"; String REMOVE_ALL_CREDENTIALS = "removeAllCredentials"; String SET_USER_VERIFIED = "setUserVerified"; + // Federated Credential Management API + // https://fedidcg.github.io/FedCM/#automation + String CANCEL_DIALOG = "cancelDialog"; + String SELECT_ACCOUNT = "selectAccount"; + String GET_ACCOUNTS = "getAccounts"; + String GET_FEDCM_TITLE = "getFedCmTitle"; + String GET_FEDCM_DIALOG_TYPE = "getFedCmDialogType"; + String SET_DELAY_ENABLED = "setDelayEnabled"; + String RESET_COOLDOWN = "resetCooldown"; static CommandPayload NEW_SESSION(Capabilities capabilities) { Require.nonNull("Capabilities", capabilities); @@ -401,4 +410,14 @@ static CommandPayload SET_CURRENT_WINDOW_SIZE(Dimension targetSize) { SET_CURRENT_WINDOW_SIZE, ImmutableMap.of("width", targetSize.width, "height", targetSize.height)); } + + static CommandPayload SELECT_ACCOUNT(int index) { + return new CommandPayload( + SELECT_ACCOUNT, ImmutableMap.of("accountIndex", index)); + } + + static CommandPayload SET_DELAY_ENABLED(boolean enabled) { + return new CommandPayload( + SET_DELAY_ENABLED, ImmutableMap.of("enabled", enabled)); + } } diff --git a/java/src/org/openqa/selenium/remote/FedCmDialogImpl.java b/java/src/org/openqa/selenium/remote/FedCmDialogImpl.java new file mode 100644 index 0000000000000..4756a376a34a0 --- /dev/null +++ b/java/src/org/openqa/selenium/remote/FedCmDialogImpl.java @@ -0,0 +1,73 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.remote; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; +import org.openqa.selenium.federatedcredentialmanagement.FederatedCredentialManagementAccount; +import org.openqa.selenium.federatedcredentialmanagement.FederatedCredentialManagementDialog; +import org.openqa.selenium.remote.DriverCommand; +import org.openqa.selenium.remote.ExecuteMethod; + +class FedCmDialogImpl implements FederatedCredentialManagementDialog { + private final ExecuteMethod executeMethod; + + FedCmDialogImpl(ExecuteMethod executeMethod) { + this.executeMethod = executeMethod; + } + + @Override + public void cancelDialog() { + executeMethod.execute(DriverCommand.CANCEL_DIALOG, null); + } + + @Override + public void selectAccount(int index) { + executeMethod.execute(DriverCommand.SELECT_ACCOUNT, ImmutableMap.of("accountIndex", index)); + } + + @Override + public String getDialogType() { + return (String) executeMethod.execute(DriverCommand.GET_FEDCM_DIALOG_TYPE, null); + } + + @Override + public String getTitle() { + Map result = (Map) executeMethod.execute(DriverCommand.GET_FEDCM_TITLE, null); + return (String) result.getOrDefault("title", null); + } + + @Override + public String getSubtitle() { + Map result = (Map) executeMethod.execute(DriverCommand.GET_FEDCM_TITLE, null); + return (String) result.getOrDefault("subtitle", null); + } + + @Override + public List getAccounts() { + List> list = (List>) executeMethod.execute(DriverCommand.GET_ACCOUNTS, null); + ArrayList accounts = new ArrayList(); + for (Map map : list) { + accounts.add(new FederatedCredentialManagementAccount(map)); + } + return accounts; + } +} diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 8f08c52d0e691..1c4753ac867b7 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -54,6 +54,7 @@ import org.openqa.selenium.ImmutableCapabilities; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.MutableCapabilities; +import org.openqa.selenium.NoAlertPresentException; import org.openqa.selenium.NoSuchFrameException; import org.openqa.selenium.NoSuchWindowException; import org.openqa.selenium.OutputType; @@ -72,6 +73,8 @@ import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.HasDevTools; +import org.openqa.selenium.federatedcredentialmanagement.HasFederatedCredentialManagement; +import org.openqa.selenium.federatedcredentialmanagement.FederatedCredentialManagementDialog; import org.openqa.selenium.interactions.Interactive; import org.openqa.selenium.interactions.Sequence; import org.openqa.selenium.internal.Require; @@ -97,6 +100,7 @@ public class RemoteWebDriver implements WebDriver, JavascriptExecutor, HasCapabilities, + HasFederatedCredentialManagement, HasVirtualAuthenticator, Interactive, PrintsPage, @@ -622,6 +626,28 @@ public void removeVirtualAuthenticator(VirtualAuthenticator authenticator) { ImmutableMap.of("authenticatorId", authenticator.getId())); } + @Override + public void setDelayEnabled(boolean enabled) { + execute(DriverCommand.SET_DELAY_ENABLED(enabled)); + } + + @Override + public void resetCooldown() { + execute(DriverCommand.RESET_COOLDOWN); + } + + @Override + public FederatedCredentialManagementDialog getFederatedCredentialManagementDialog() { + FederatedCredentialManagementDialog dialog = new FedCmDialogImpl(executeMethod); + try { + // As long as this does not throw, we're good. + dialog.getDialogType(); + return dialog; + } catch (NoAlertPresentException e) { + return null; + } + } + /** * Override this to be notified at key points in the execution of a command. * diff --git a/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java b/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java index 7542d80896249..ff7adad37bd8c 100644 --- a/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java +++ b/java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java @@ -26,6 +26,7 @@ import static org.openqa.selenium.remote.DriverCommand.ADD_COOKIE; import static org.openqa.selenium.remote.DriverCommand.ADD_CREDENTIAL; import static org.openqa.selenium.remote.DriverCommand.ADD_VIRTUAL_AUTHENTICATOR; +import static org.openqa.selenium.remote.DriverCommand.CANCEL_DIALOG; import static org.openqa.selenium.remote.DriverCommand.CLEAR_ELEMENT; import static org.openqa.selenium.remote.DriverCommand.CLICK_ELEMENT; import static org.openqa.selenium.remote.DriverCommand.CLOSE; @@ -39,6 +40,7 @@ import static org.openqa.selenium.remote.DriverCommand.FIND_ELEMENTS; import static org.openqa.selenium.remote.DriverCommand.FULLSCREEN_CURRENT_WINDOW; import static org.openqa.selenium.remote.DriverCommand.GET; +import static org.openqa.selenium.remote.DriverCommand.GET_ACCOUNTS; import static org.openqa.selenium.remote.DriverCommand.GET_ALL_COOKIES; import static org.openqa.selenium.remote.DriverCommand.GET_ALL_SESSIONS; import static org.openqa.selenium.remote.DriverCommand.GET_APP_CACHE_STATUS; @@ -55,6 +57,8 @@ import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_TAG_NAME; import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_TEXT; import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_VALUE_OF_CSS_PROPERTY; +import static org.openqa.selenium.remote.DriverCommand.GET_FEDCM_DIALOG_TYPE; +import static org.openqa.selenium.remote.DriverCommand.GET_FEDCM_TITLE; import static org.openqa.selenium.remote.DriverCommand.GET_LOCATION; import static org.openqa.selenium.remote.DriverCommand.GET_LOG; import static org.openqa.selenium.remote.DriverCommand.GET_NETWORK_CONNECTION; @@ -75,10 +79,13 @@ import static org.openqa.selenium.remote.DriverCommand.REMOVE_ALL_CREDENTIALS; import static org.openqa.selenium.remote.DriverCommand.REMOVE_CREDENTIAL; import static org.openqa.selenium.remote.DriverCommand.REMOVE_VIRTUAL_AUTHENTICATOR; +import static org.openqa.selenium.remote.DriverCommand.RESET_COOLDOWN; import static org.openqa.selenium.remote.DriverCommand.SCREENSHOT; +import static org.openqa.selenium.remote.DriverCommand.SELECT_ACCOUNT; import static org.openqa.selenium.remote.DriverCommand.SEND_KEYS_TO_ELEMENT; import static org.openqa.selenium.remote.DriverCommand.SET_ALERT_CREDENTIALS; import static org.openqa.selenium.remote.DriverCommand.SET_BROWSER_ONLINE; +import static org.openqa.selenium.remote.DriverCommand.SET_DELAY_ENABLED; import static org.openqa.selenium.remote.DriverCommand.SET_LOCATION; import static org.openqa.selenium.remote.DriverCommand.SET_NETWORK_CONNECTION; import static org.openqa.selenium.remote.DriverCommand.SET_SCREEN_ORIENTATION; @@ -220,6 +227,16 @@ public AbstractHttpCommandCodec() { defineCommand(REMOVE_CREDENTIAL, delete(webauthnId + "/credentials/:credentialId")); defineCommand(REMOVE_ALL_CREDENTIALS, delete(webauthnId + "/credentials")); defineCommand(SET_USER_VERIFIED, post(webauthnId + "/uv")); + + // Federated Credential Management API + String fedcm = sessionId + "/fedcm"; + defineCommand(CANCEL_DIALOG, post(fedcm + "/canceldialog")); + defineCommand(SELECT_ACCOUNT, post(fedcm + "/selectaccount")); + defineCommand(GET_ACCOUNTS, get(fedcm + "/accountlist")); + defineCommand(GET_FEDCM_TITLE, get(fedcm + "/gettitle")); + defineCommand(GET_FEDCM_DIALOG_TYPE, get(fedcm + "/getdialogtype")); + defineCommand(SET_DELAY_ENABLED, post(fedcm + "/setdelayenabled")); + defineCommand(RESET_COOLDOWN, post(fedcm + "/resetCooldown")); } protected static CommandSpec delete(String path) { diff --git a/java/test/org/openqa/selenium/environment/webserver/FedCmIdAssertion.java b/java/test/org/openqa/selenium/environment/webserver/FedCmIdAssertion.java new file mode 100644 index 0000000000000..78fe624adc944 --- /dev/null +++ b/java/test/org/openqa/selenium/environment/webserver/FedCmIdAssertion.java @@ -0,0 +1,52 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.environment.webserver; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.UncheckedIOException; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.openqa.selenium.remote.http.Contents; +import org.openqa.selenium.remote.http.HttpHandler; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.openqa.selenium.remote.http.UrlPath; + +/** + * Implements FedCM's ID assertion endpoint. + * + * https://fedidcg.github.io/FedCM/#idp-api-id-assertion-endpoint + */ +class FedCmIdAssertion implements HttpHandler { + + private static final String RESPONSE_STRING = + "{\"token\": \"%s\"}"; + + @Override + public HttpResponse execute(HttpRequest req) throws UncheckedIOException { + HttpResponse response = new HttpResponse(); + response.setHeader("Content-Type", "application/json"); + response.setHeader("Cache-Control", "no-store"); + + response.setContent( + Contents.string(String.format(RESPONSE_STRING, "a token"), UTF_8)); + + return response; + } +} diff --git a/java/test/org/openqa/selenium/environment/webserver/HandlersForTests.java b/java/test/org/openqa/selenium/environment/webserver/HandlersForTests.java index f870fc7010c16..71924f60dd5a4 100644 --- a/java/test/org/openqa/selenium/environment/webserver/HandlersForTests.java +++ b/java/test/org/openqa/selenium/environment/webserver/HandlersForTests.java @@ -55,8 +55,10 @@ public HandlersForTests(String hostname, int port, Path tempPageDir) { .addHeader("Content-Type", MediaType.HTML_UTF_8.toString()) .setContent(Contents.string("

authorized

", UTF_8))) .with(new BasicAuthenticationFilter("test", "test")), + Route.get("/.well-known/web-identity").to(WellKnownWebIdentityHandler::new), Route.get("/echo").to(EchoHandler::new), Route.get("/cookie").to(CookieHandler::new), + Route.post("/fedcm/id_assertion").to(FedCmIdAssertion::new), Route.get("/encoding").to(EncodingHandler::new), Route.matching(req -> req.getUri().startsWith("/generated/")) .to(() -> new GeneratedJsTestHandler("/generated")), diff --git a/java/test/org/openqa/selenium/environment/webserver/WellKnownWebIdentityHandler.java b/java/test/org/openqa/selenium/environment/webserver/WellKnownWebIdentityHandler.java new file mode 100644 index 0000000000000..e2235637cf39b --- /dev/null +++ b/java/test/org/openqa/selenium/environment/webserver/WellKnownWebIdentityHandler.java @@ -0,0 +1,48 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.environment.webserver; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.UncheckedIOException; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.openqa.selenium.remote.http.Contents; +import org.openqa.selenium.remote.http.HttpHandler; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.openqa.selenium.remote.http.UrlPath; + +class WellKnownWebIdentityHandler implements HttpHandler { + + private static final String RESPONSE_STRING = + "{\"provider_urls\": [\"%s\"]}"; + + @Override + public HttpResponse execute(HttpRequest req) throws UncheckedIOException { + HttpResponse response = new HttpResponse(); + response.setHeader("Content-Type", "application/json"); + response.setHeader("Cache-Control", "no-store"); + String targetLocation = UrlPath.relativeToContext(req, "/fedcm/fedcm.json"); + + response.setContent( + Contents.string(String.format(RESPONSE_STRING, targetLocation), UTF_8)); + + return response; + } +} diff --git a/java/test/org/openqa/selenium/federatedcredentialmanagement/BUILD.bazel b/java/test/org/openqa/selenium/federatedcredentialmanagement/BUILD.bazel new file mode 100644 index 0000000000000..1f12776a5ecb6 --- /dev/null +++ b/java/test/org/openqa/selenium/federatedcredentialmanagement/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//java:defs.bzl", "JUNIT5_DEPS", "java_selenium_test_suite") + +java_selenium_test_suite( + name = "LargeTests", + size = "large", + srcs = glob(["*.java"]), + browsers = [ + "chrome", + ], + deps = [ + "//java/src/org/openqa/selenium/chrome", + "//java/src/org/openqa/selenium:core", + "//java/src/org/openqa/selenium/remote", + "//java/src/org/openqa/selenium/support", + "//java/test/org/openqa/selenium/environment", + "//java/test/org/openqa/selenium/testing:annotations", + "//java/test/org/openqa/selenium/testing:test-base", + "//java/test/org/openqa/selenium/testing/drivers", + artifact("org.junit.jupiter:junit-jupiter-api"), + artifact("org.assertj:assertj-core"), + ] + JUNIT5_DEPS, +) diff --git a/java/test/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementTest.java b/java/test/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementTest.java new file mode 100644 index 0000000000000..a1bddc115f768 --- /dev/null +++ b/java/test/org/openqa/selenium/federatedcredentialmanagement/FederatedCredentialManagementTest.java @@ -0,0 +1,135 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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.openqa.selenium.federatedcredentialmanagement; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.Fail.fail; +import static org.assertj.core.api.InstanceOfAssertFactories.MAP; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.InvalidSelectorException; +import org.openqa.selenium.JavascriptException; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.federatedcredentialmanagement.FederatedCredentialManagementDialog; +import org.openqa.selenium.federatedcredentialmanagement.HasFederatedCredentialManagement; +import org.openqa.selenium.support.ui.Wait; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.openqa.selenium.testing.JupiterTestBase; +import org.openqa.selenium.testing.drivers.Browser; + +class FederatedCredentialManagementTest extends JupiterTestBase { + + private JavascriptExecutor jsAwareDriver; + private HasFederatedCredentialManagement fedcmDriver; + + @BeforeEach + public void setup() { + ChromeOptions options = (ChromeOptions) Browser.CHROME.getCapabilities(); + //options.setAcceptInsecureCerts(true); + options.addArguments(String.format("host-resolver-rules=MAP localhost:443 localhost:%d", getSecurePort())); + options.addArguments("ignore-certificate-errors"); + localDriver = seleniumExtension.createNewDriver(options); + + assumeThat(localDriver).isInstanceOf(HasFederatedCredentialManagement.class); + jsAwareDriver = (JavascriptExecutor) localDriver; + fedcmDriver = (HasFederatedCredentialManagement) localDriver; + localDriver.get(appServer.whereIsSecure("/fedcm/fedcm.html")); + } + + private Object triggerFedCm() { + return jsAwareDriver.executeScript("triggerFedCm()"); + } + + private void waitForDialog() { + WebDriverWait wait = new WebDriverWait(localDriver, Duration.ofSeconds(5)); + wait.until(driver -> ((HasFederatedCredentialManagement) driver).getFederatedCredentialManagementDialog() != null); + } + + private int getSecurePort() { + String urlString = appServer.whereIsSecure("/"); + try { + return new URL(urlString).getPort(); + } catch (MalformedURLException ex) { + // This should not happen. + return 0; + } + } + + @Test + void testDismissDialog() { + fedcmDriver.setDelayEnabled(false); + assertNull(fedcmDriver.getFederatedCredentialManagementDialog()); + + Object response = triggerFedCm(); + + waitForDialog(); + + FederatedCredentialManagementDialog dialog = fedcmDriver.getFederatedCredentialManagementDialog(); + + assertEquals("Sign in to localhost with localhost", dialog.getTitle()); + assertEquals("AccountChooser", dialog.getDialogType()); + + dialog.cancelDialog(); + + // Check that the dialog was indeed closed (i.e. the promise now resolves). + assertThrows(JavascriptException.class, () -> { + try { + jsAwareDriver.executeScript("await promise"); + } catch (InvalidSelectorException ex) { + // Due to a bug in Chromedriver (https://crbug.com/1454586), we may + // get an invalid selector exception here instead of a JavascriptException. + // Turn it into a JavascriptException to make the test pass for now. + throw new JavascriptException(ex.getMessage(), ex); + } + }); + } + + @Test + void testSelectAccount() { + fedcmDriver.setDelayEnabled(false); + assertNull(fedcmDriver.getFederatedCredentialManagementDialog()); + + Object response = triggerFedCm(); + + waitForDialog(); + + FederatedCredentialManagementDialog dialog = fedcmDriver.getFederatedCredentialManagementDialog(); + + assertEquals("Sign in to localhost with localhost", dialog.getTitle()); + assertEquals("AccountChooser", dialog.getDialogType()); + + dialog.selectAccount(0); + + response = jsAwareDriver.executeScript("return await promise"); + assertThat(response).asInstanceOf(MAP).containsEntry("token", "a token"); + } +}