- gellweiler / README.md
- Testing HTTP Basic Authentication in Selenium
- Introduction
- How HTTP Basic Authentication works
- Method 1: Passing credentials via URL
- Method 2: Using the Authorization HTTP request header
- Making it work in Selenium
- Requirements
- Writing the test
- Note on Security
- Selenium Basic Authentication via URL
- For Mozilla Firefox,
- For Chrome,
gellweiler / README.md
This java class provides a simple way to achieve basic auth in recent versions of selenium (current version 3.x) with recent versions of google chrome (current year 2020). It dynamically generates a tiny extension that will add the Authorization header for you.
// Add basic auth header with a chrome extension on every request File authExtension = new SeleniumChromeAuthExtensionBuilder() .withBasicAuth("Ali Baba", "Open sesame") .withBaseUrl("https://example.org/*") .build(); try < ChromeOptions chromeOptions = new ChromeOptions(); chromeOptions = chromeOptions.addExtensions(authExtension); webDriver = new ChromeDriver(chromeOptions); >finally
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
import org . apache . commons . lang3 . StringEscapeUtils ; |
import java . io . File ; |
import java . io . FileOutputStream ; |
import java . nio . charset . StandardCharsets ; |
import java . util . Base64 ; |
import java . util . zip . ZipEntry ; |
import java . util . zip . ZipOutputStream ; |
/** |
* This class can be used to generate a minimal browser extension that |
* will force Chrome to send out an Authorize header on every request. |
* |
* This is the only reliable way that has worked for me with Selenium 3 |
* to use basic auth. |
* |
* Alternatives to this approach: |
* — Add the basic auth credentials to the url (does not work in recent versions of FF and Chrome) |
* — Using a proxy that adds the header (LittleProxy). This should probably work, |
* but https with proxies can be a pain in the ass. Also the last release of LittleProxy is from 2017. |
* Launching an extra server for every test would also unnecessarily increase the complexity of tests. |
* — Using selenium as the user would by filling out the username, password text boxes and clicking on the login button. |
* This works, but with login forms adding captcha and other deterrents to stop bots can be unreliable. |
* — Use a cookie for authentication and add that to the selenium browser. This works great if you can get |
* a valid session cookie. |
* |
* Useful links: |
* — https://stackoverflow.com/a/35293026 |
* — https://devopsqa.wordpress.com/2018/08/05/handle-basic-authentication-in-selenium-for-chrome-browser/ |
* — https://stackoverflow.com/a/27936481 |
* — https://sqa.stackexchange.com/questions/12892/how-to-send-basic-authentication-headers-in-selenium |
*/ |
public class SeleniumChromeAuthExtensionBuilder |
private String headerJsCode = «» ; |
private String baseUrl = «» ; |
/** |
* Add a header to every request. |
* |
* @param name The name of the header to add. |
* @param value The value of the header. |
*/ |
public SeleniumChromeAuthExtensionBuilder withStaticHeader ( String name , String value ) |
String nameEscaped = StringEscapeUtils . escapeEcmaScript ( name ); |
String valueEscaped = StringEscapeUtils . escapeEcmaScript ( value ); |
headerJsCode += «headers.push(); \n » ; |
return this ; |
> |
/** |
* Add basic auth credentials to every requests. |
* |
* @param username The username for authentication |
* @param password The password for authentication |
*/ |
public SeleniumChromeAuthExtensionBuilder withBasicAuth ( String username , String password ) |
String encodeUserPass = Base64 . getEncoder (). encodeToString (( username + «:» + password ). getBytes ( StandardCharsets . UTF_8 )); |
return withStaticHeader ( «Authorization» , «Basic » + encodeUserPass ); |
> |
/** |
* If you set a base url only requests to urls starting with this base url will be modified. |
* Can contain wildcards (*). The special value matches all urls (default). |
*/ |
public SeleniumChromeAuthExtensionBuilder withBaseUrl ( String baseUrl ) |
this . baseUrl = baseUrl ; |
return this ; |
> |
/** |
* Builds the extension as a zip archive. |
* Please delete this file after installing it. |
* |
* @return The generated zip file |
*/ |
public File build () throws Exception |
File tempFile = File . createTempFile ( «selenium-chrome-auth-» , «.tmp.zip» ); |
ZipOutputStream out = new ZipOutputStream ( new FileOutputStream ( tempFile )); |
// Add manifest.json |
ZipEntry e = new ZipEntry ( «manifest.json» ); |
out . putNextEntry ( e ); |
out . write ( generateManifestJson (). getBytes ( StandardCharsets . UTF_8 )); |
out . closeEntry (); |
> |
// Add background.js |
ZipEntry e = new ZipEntry ( «background.js» ); |
out . putNextEntry ( e ); |
out . write ( generateBackroundJs (). getBytes ( StandardCharsets . UTF_8 )); |
out . closeEntry (); |
> |
out . close (); |
return tempFile ; |
> |
private String generateBackroundJs () |
return «chrome.webRequest.onBeforeSendHeaders.addListener( \n » + |
» function(e) < \n " + |
» var headers = e.requestHeaders; \n » + |
headerJsCode + |
» return < requestHeaders: headers >; \n » + |
» >, \n » + |
» , \n » + |
» [‘blocking’, ‘requestHeaders’ , ‘extraHeaders’] \n » + |
«);» ; |
> |
private String generateManifestJson () |
return » < \n " + |
» \» manifest_version \» : 2, \n » + |
» \» name \» : \» Authentication for selenium tests \» , \n » + |
» \» version \» : \» 1.0.0 \» , \n » + |
» \» permissions \» : [ \» \» , \» webRequest \» , \» webRequestBlocking \» ], \n » + |
» \» background \» : < \n " + |
» \» scripts \» : [ \» background.js \» ] \n » + |
» > \n » + |
» >» ; |
> |
> |
Testing HTTP Basic Authentication in Selenium
Use Selenium WebDriver to test web applications that are password-protected using HTTP Basic Authentication.
Photo by ‘Muhammad Zaqy Al Fattah’ on Unsplash
Table of contents
Introduction
HTTP Basic Authentication is an authentication method that’s built into the HTTP spec. It can be a very easy means of implementing authentication on a web application, and is a common way to secure non-production environments like QA and Staging environments without having to add an application-level auth mechanism.
Browsers have built-in support for supporting HTTP Basic Authentication. When a browser request receives an HTTP 401 Unauthorized response code, together with a WWW-Authenticate: Basic realm=»» response header field (where can be any user-defined string), it will automatically prompt the user to enter a username and password. When the user enters their credentials, the request will be re-issued with the credentials passed via an Authorization request header and, if the credentials are correct, the user will be authorized to access the protected page.
How HTTP Basic Authentication works
There are two methods of authenticating via Basic Auth:
Method 1: Passing credentials via URL
Authentication credentials can be passed directly in the URL, allowing you to bypass the browser prompt. To pass authentication credentials in the URL, you must use the following format:
http://username:password@www.example.com
The username and password are separated by a colon (:), then an @ is placed before the URL to be requested.
If the username and password contains : and @ in their values, you should first urlencode the values.
const username = encodeURIComponent("username"); const password = encodeURIComponent("pass!word@123"); const url = `https://$username>:$password>@www.example.com`;
Method 2: Using the Authorization HTTP request header
In situations where you’re making an API call directly, such as in an integration test or when using a utility like curl , credentials can be populated directly in the Authorization HTTP request header via the following steps:
- Create a string combining the username and password separated by a colon (:) i.e username:password.
- This string should be base64 encoded
- The authorization header should be constructed as follows: Authorization: Basic [encoded string]
Making it work in Selenium
Let’s create a Selenium script that interacts with a site that’s secured via HTTP Basic Authentication. This article assumes you are already fairly familiar with Selenium. For a detailed introduction into using Selenium, check the official documentation as well as our tutorial on writing end-to-end tests with Selenium.
Requirements
- Selenium
- Node (Since we will be using Selenium’s JavaScript library)
- npm
- Chrome WebDriver
Create a new NPM project using npm init and follow through the prompt.
Your package.json file should be looking like this
"name": "seledriving", "version": "1.0.0", "main": "index.js", "author": "sdkcodes", "license": "MIT" >
Install Selenium using npm install selenium-webdriver
Writing the test
Because Selenium does not natively provide us with a way to modify the headers of a request, we will be making our test by passing the credentials via the URL.
const Builder, By > = require("selenium-webdriver"); const chrome = require("selenium-webdriver/chrome"); (async function openChromeTest() try let options = new chrome.Options(); let driver = await new Builder().setChromeOptions(options).forBrowser("chrome").build(); await driver.get("https://admin:admi@the-internet.herokuapp.com/basic_auth"); let text = await driver.findElement(By.css("p")).getText(); console.log(text); await driver.quit(); > catch (error) console.log(error); > >)();
When the browser is launched, if our credentials is valid, we should see a success screen
But if we supplied invalid credentials, we will be prompted for a username and password as seen below:
Note on Security
The Basic Authentication mechanism does not provide confidentiality on the data transmitted since the credentials are transmitted in plain text. At best, the credentials are base64 encoded which can easily be decoded. If you’re authenticating via Basic Auth, you should do so over HTTPS to ensure both the user credentials as well as the contents of the protected site are not visible to malicious third parties.
Selenium Basic Authentication via URL
There are instances where you would work with applications that have their security behind the basic HTTP Authentication, also known as the Basic Authentication. Before you can be granted access to these sites or apps, you will be required to run some credentials through the website where you are requesting a page. If you cannot run those credentials through the website, you will have to deal with a pop up at a system level that prompts users for a username and password. This situation renders your Selenium helpless.
In the initial versions before Selenium 2, it is possible to execute it through the injection of credentials into a custom header. However, due to some advancements, another way to go about this is the Browsermob proxy, which is quite complex. We will discuss a simpler way here.
It is essential to recognize that the basic authentication feature is still working correctly with geckodriver v0.18.0, Mozilla Firefox 53.0, Selenium 3.4.0, Google Chrome 60.x, and chromedriver v2.31.488763, through Selenium Java bindings. This is a tested and working code that successfully opens using a valid set of credentials to access the URL.
For Mozilla Firefox,
import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; public class BasicAuthentication_FF < public static void main(String[] args) < System.setProperty("webdriver.gecko.driver", "C:\\Utility\\BrowserDrivers\\geckodriver.exe"); WebDriver driver = new FirefoxDriver(); driver.navigate().to("http://admin:admin@the-internet.herokuapp.com/basic_auth"); >>
For Chrome,
import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; public class BasicAuthentication_Chrome < public static void main(String[] args) < System.setProperty("webdriver.chrome.driver", "C:\\Utility\\BrowserDrivers\\chromedriver.exe"); ChromeOptions options = new ChromeOptions(); options.addArguments("start-maximized"); options.addArguments("disable-infobars"); options.addArguments("--disable-extensions"); WebDriver driver = new ChromeDriver(options); driver.navigate().to("http://admin:admin@the-internet.herokuapp.com/basic_auth"); >>
You should note that the basic authentication via URL is blocked only for sub-resources, thus creating the possibility of using it on the domain as follows:
driver.get("http://admin:admin@localhost:8080"); driver.get("http://localhost:8080/project");
It is also possible to create a small extension to automatically set the credentials when they are requested as follows:
options = webdriver.ChromeOptions() options.add_extension(r'C:\dev\credentials.zip')