Skip to content

Commit

Permalink
Add support for authenticating using Basic and Digest auth
Browse files Browse the repository at this point in the history
This currently uses CDP, and so is limited to chromium-derived
browsers, but the APIs are generic and can be applied to other
methods.
  • Loading branch information
shs96c committed Sep 7, 2020
1 parent 98576ce commit 6c198bc
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 38 deletions.
22 changes: 22 additions & 0 deletions java/client/src/org/openqa/selenium/Credentials.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
// 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;

/**
* Marker interface for credentials used for authenticating a browser to a
* site, typically via {@link UsernameAndPassword} and Basic or Digest
* authentication.
*/
public interface Credentials {
}
32 changes: 31 additions & 1 deletion java/client/src/org/openqa/selenium/HasAuthentication.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// 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;

import org.openqa.selenium.internal.Require;
Expand All @@ -7,12 +24,25 @@
import java.util.function.Supplier;

/**
* Indicates that a driver supports authentication in some way.
* Indicates that a driver supports authenticating to a website in some way.
*
* @see Credentials
* @see UsernameAndPassword
*/
public interface HasAuthentication {

/**
* Registers a check for whether a set of {@link Credentials} should be
* used for a particular site, identified by its URI. If called multiple
* times, the credentials will be checked in the order they've been added
* and the first one to match will be used.
*/
void register(Predicate<URI> whenThisMatches, Supplier<Credentials> useTheseCredentials);

/**
* As {@link #register(Predicate, Supplier)} but attempts to apply the
* credentials for any request for authorization.
*/
default void register(Supplier<Credentials> alwaysUseTheseCredentials) {
Require.nonNull("Credentials", alwaysUseTheseCredentials);

Expand Down
21 changes: 21 additions & 0 deletions java/client/src/org/openqa/selenium/UsernameAndPassword.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
// 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;

import org.openqa.selenium.internal.Require;

import java.util.function.Supplier;

/**
* A combination of username and password to use when authenticating a
* browser with a website.
*/
public class UsernameAndPassword implements Credentials {

private final String username;
Expand Down
116 changes: 93 additions & 23 deletions java/client/src/org/openqa/selenium/chromium/ChromiumDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@

package org.openqa.selenium.chromium;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.BuildInfo;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.Credentials;
import org.openqa.selenium.HasAuthentication;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.devtools.CdpInfo;
import org.openqa.selenium.devtools.CdpVersionFinder;
import org.openqa.selenium.devtools.Connection;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.DevToolsException;
import org.openqa.selenium.devtools.HasDevTools;
import org.openqa.selenium.devtools.idealized.fetch.Fetch;
import org.openqa.selenium.devtools.idealized.fetch.model.RequestPattern;
import org.openqa.selenium.devtools.noop.NoOpCdpInfo;
import org.openqa.selenium.html5.LocalStorage;
import org.openqa.selenium.html5.Location;
Expand All @@ -46,29 +52,41 @@
import org.openqa.selenium.remote.html5.RemoteLocationContext;
import org.openqa.selenium.remote.html5.RemoteWebStorage;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.mobile.RemoteNetworkConnection;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Logger;

/**
* A {@link WebDriver} implementation that controls a Chromium browser running on the local machine.
* This class is provided as a convenience for easily testing the Chromium browser. The control server
* which each instance communicates with will live and die with the instance.
*
* <p>
* To avoid unnecessarily restarting the ChromiumDriver server with each instance, use a
* {@link RemoteWebDriver} coupled with the desired WebDriverService, which is managed
* separately.
*
* <p>
* Note that unlike ChromiumDriver, RemoteWebDriver doesn't directly implement
* role interfaces such as {@link LocationContext} and {@link WebStorage}.
* Therefore, to access that functionality, it needs to be
* {@link org.openqa.selenium.remote.Augmenter augmented} and then cast
* to the appropriate interface.
*/
public class ChromiumDriver extends RemoteWebDriver
implements HasDevTools, HasLogEvents, HasTouchScreen, LocationContext, NetworkConnection, WebStorage {
public class ChromiumDriver extends RemoteWebDriver implements
HasAuthentication,
HasDevTools,
HasLogEvents,
HasTouchScreen,
LocationContext,
NetworkConnection,
WebStorage {

private static final Logger LOG = Logger.getLogger(ChromiumDriver.class.getName());
private final RemoteLocationContext locationContext;
Expand All @@ -77,6 +95,8 @@ public class ChromiumDriver extends RemoteWebDriver
private final RemoteNetworkConnection networkConnection;
private final Optional<Connection> connection;
private final Optional<DevTools> devTools;
private final Map<Predicate<URI>, Supplier<Credentials>> allKnownCredentials = new LinkedHashMap<>();
private boolean authenticationInitialized = false;

protected ChromiumDriver(CommandExecutor commandExecutor, Capabilities capabilities, String capabilityKey) {
super(commandExecutor, capabilities);
Expand All @@ -87,33 +107,33 @@ protected ChromiumDriver(CommandExecutor commandExecutor, Capabilities capabilit

HttpClient.Factory factory = HttpClient.Factory.createDefault();
connection = ChromiumDevToolsLocator.getChromeConnector(
factory,
getCapabilities(),
capabilityKey);
factory,
getCapabilities(),
capabilityKey);

CdpInfo cdpInfo = new CdpVersionFinder().match(getCapabilities().getVersion())
.orElseGet(() -> {
LOG.warning(
String.format(
"Unable to find version of CDP to use for %s. You may need to " +
"include a dependency on a specific version of the CDP using " +
"something similar to " +
"`org.seleniumhq.selenium:selenium-devtools-v86:%s` where the " +
"version (\"v86\") matches the version of the chromium-based browser " +
"you're using and the version number of the artifact is the same " +
"as Selenium's.",
capabilities.getVersion(),
new BuildInfo().getReleaseLabel()));
return new NoOpCdpInfo();
});
LOG.warning(
String.format(
"Unable to find version of CDP to use for %s. You may need to " +
"include a dependency on a specific version of the CDP using " +
"something similar to " +
"`org.seleniumhq.selenium:selenium-devtools-v86:%s` where the " +
"version (\"v86\") matches the version of the chromium-based browser " +
"you're using and the version number of the artifact is the same " +
"as Selenium's.",
capabilities.getVersion(),
new BuildInfo().getReleaseLabel()));
return new NoOpCdpInfo();
});

devTools = connection.map(conn -> new DevTools(cdpInfo.getDomains(), conn));
}

@Override
public void setFileDetector(FileDetector detector) {
throw new WebDriverException(
"Setting the file detector only works on remote webdriver instances obtained " +
"Setting the file detector only works on remote webdriver instances obtained " +
"via RemoteWebDriver");
}

Expand All @@ -123,6 +143,56 @@ public <X> void onLogEvent(EventType<X> kind) {
kind.initializeLogger(this);
}

@Override
public void register(Predicate<URI> whenThisMatches, Supplier<Credentials> useTheseCredentials) {
Require.nonNull("Check to use to see how we should authenticate", whenThisMatches);
Require.nonNull("Credentials to use when authenticating", useTheseCredentials);

allKnownCredentials.put(whenThisMatches, useTheseCredentials);

if (!authenticationInitialized) {
DevTools devTools = this.devTools.orElseThrow(() -> new DevToolsException("Unable to make devtools connection"));
devTools.createSessionIfThereIsNotOne();

RequestPattern pattern = new RequestPattern(Optional.of("*"), Optional.empty());

Fetch fetch = devTools.getDomains().fetch();

// TODO: This will fight with NetworkInterceptor...
devTools.addListener(
fetch.requestPaused(),
requestPaused -> {
Optional<HttpRequest> maybeRequest = requestPaused.getRequest();
if (!maybeRequest.isPresent()) {
devTools.send(fetch.continueRequest(requestPaused.getRequestId()));
return;
}
devTools.send(fetch.continueRequest(requestPaused.getRequestId()));
});
devTools.addListener(
fetch.authRequired(),
authRequest -> {
try {
URI uri = new URI(authRequest.getChallenge().getOrigin());
Optional<Credentials> maybeCredentials = allKnownCredentials.entrySet().stream()
.filter(entry -> entry.getKey().test(uri))
.map(entry -> entry.getValue().get())
.findFirst();

if (!maybeCredentials.isPresent()) {
devTools.send(fetch.cancelAuth(authRequest.getRequestId()));
} else {
devTools.send(fetch.authorize(authRequest.getRequestId(), maybeCredentials.get()));
}
} catch (URISyntaxException e) {
throw new DevToolsException(e);
}
}
);
devTools.send(fetch.enable(Optional.of(ImmutableList.of(pattern)), true));
}
}

@Override
public LocalStorage getLocalStorage() {
return webStorage.getLocalStorage();
Expand Down Expand Up @@ -179,8 +249,8 @@ public Map<String, Object> executeCdpCommand(String commandName, Map<String, Obj

@SuppressWarnings("unchecked")
Map<String, Object> toReturn = (Map<String, Object>) getExecuteMethod().execute(
ChromiumDriverCommand.EXECUTE_CDP_COMMAND,
ImmutableMap.of("cmd", commandName, "params", parameters));
ChromiumDriverCommand.EXECUTE_CDP_COMMAND,
ImmutableMap.of("cmd", commandName, "params", parameters));

return ImmutableMap.copyOf(toReturn);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ private Optional<CdpInfo> findNearestMatch(int version) {

for (CdpInfo info : infos) {
if (info.getMajorVersion() == version) {
LOG.info(String.format("Found exact CDP implementation for version %d", version));
return Optional.of(info);
}

Expand All @@ -141,6 +142,12 @@ private Optional<CdpInfo> findNearestMatch(int version) {
version,
nearestMatch == null ? "a no-op implementation" : nearestMatch.getMajorVersion()));

if (nearestMatch == null) {
LOG.info(String.format("Unable to find CDP implementation matching %d.", version));
} else {
LOG.info(String.format("Found CDP implementation for version %d of %d", version, nearestMatch.getMajorVersion()));
}

return Optional.ofNullable(nearestMatch);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

package org.openqa.selenium.devtools.idealized.fetch;

import org.openqa.selenium.Credentials;
import org.openqa.selenium.devtools.Command;
import org.openqa.selenium.devtools.Event;
import org.openqa.selenium.devtools.idealized.fetch.model.AuthRequired;
import org.openqa.selenium.devtools.idealized.fetch.model.RequestId;
import org.openqa.selenium.devtools.idealized.fetch.model.RequestPattern;
import org.openqa.selenium.devtools.idealized.fetch.model.RequestPaused;
Expand All @@ -33,7 +35,13 @@ public interface Fetch {

Event<RequestPaused> requestPaused();

Event<AuthRequired> authRequired();

Command<Void> fulfillRequest(RequestId requestId, HttpResponse response);

Command<Void> continueRequest(RequestId requestId);

Command<Void> cancelAuth(RequestId requestId);

Command<Void> authorize(RequestId requestId, Credentials credentials);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.devtools.idealized.fetch.model;

import org.openqa.selenium.internal.Require;

public class AuthChallenge {

private final String scheme;
private final String origin;
private final String realm;

public AuthChallenge(String scheme, String origin, String realm) {
this.scheme = Require.nonNull("Scheme", scheme);
this.origin = Require.nonNull("Origin", origin);
this.realm = Require.nonNull("Realm", realm);
}

public String getScheme() {
return scheme;
}

public String getOrigin() {
return origin;
}

public String getRealm() {
return realm;
}
}
Loading

0 comments on commit 6c198bc

Please sign in to comment.