diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index 878587afd52ee..2083ad3354d94 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -302,11 +302,14 @@ class NetworkRequest { } if (!credentials) return false; - const origin = aChannel.URI.scheme + '://' + aChannel.URI.hostPort; - if (credentials.origin && origin.toLowerCase() !== credentials.origin.toLowerCase()) + const list = Array.isArray(credentials) ? credentials : [credentials]; + const origin = (aChannel.URI.scheme + '://' + aChannel.URI.hostPort).toLowerCase(); + const exact = list.find(c => c.origin && origin === c.origin.toLowerCase()); + const chosen = exact || (list.length === 1 && !list[0].origin ? list[0] : null); + if (!chosen) return false; - authInfo.username = credentials.username; - authInfo.password = credentials.password; + authInfo.username = chosen.username; + authInfo.password = chosen.password; // This will produce a new request with respective auth header set. // It will have the same id as ours. We expect it to arrive as new request and // will treat it as our own redirect. diff --git a/browser_patches/firefox/juggler/protocol/PrimitiveTypes.js b/browser_patches/firefox/juggler/protocol/PrimitiveTypes.js index ca6d1ec4f60f2..a3a7d3e736c8a 100644 --- a/browser_patches/firefox/juggler/protocol/PrimitiveTypes.js +++ b/browser_patches/firefox/juggler/protocol/PrimitiveTypes.js @@ -83,6 +83,18 @@ t.Array = function(scheme) { } } +t.Either = function(...schemes) { + return function(x, details = {}, path = ['']) { + for (const scheme of schemes) { + const nestedDetails = {}; + if (checkScheme(scheme, x, nestedDetails, path.slice())) + return true; + } + details.error = `Expected "${path.join('.')}" to match one of the alternative schemes; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; + return false; + } +} + t.Recursive = function(types, schemeName) { return function(x, details = {}, path = ['']) { const scheme = types[schemeName]; diff --git a/browser_patches/firefox/juggler/protocol/Protocol.js b/browser_patches/firefox/juggler/protocol/Protocol.js index 6aca7c22fced5..d6dc5b18e2ffc 100644 --- a/browser_patches/firefox/juggler/protocol/Protocol.js +++ b/browser_patches/firefox/juggler/protocol/Protocol.js @@ -157,6 +157,8 @@ networkTypes.HTTPCredentials = { origin: t.Optional(t.String), }; +networkTypes.HTTPCredentialsList = t.Array(networkTypes.HTTPCredentials); + networkTypes.SecurityDetails = { protocol: t.String, subjectName: t.String, @@ -271,7 +273,8 @@ const Browser = { 'setHTTPCredentials': { params: { browserContextId: t.Optional(t.String), - credentials: t.Nullable(networkTypes.HTTPCredentials), + // Accept a single credentials object (legacy) or an array for multiple origins. + credentials: t.Nullable(t.Either(networkTypes.HTTPCredentials, networkTypes.HTTPCredentialsList)), }, }, 'setRequestInterception': { diff --git a/docs/src/api/class-apirequest.md b/docs/src/api/class-apirequest.md index d27e22b03410a..a4ffe9e64f1e5 100644 --- a/docs/src/api/class-apirequest.md +++ b/docs/src/api/class-apirequest.md @@ -31,6 +31,9 @@ for all status codes. ### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%% * since: v1.16 +### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-csharp-java-python-%% +* since: v1.16 + ### option: APIRequest.newContext.proxy = %%-browser-option-proxy-%% * since: v1.16 diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index a5679cf5a96a3..4df39c063b5b5 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1523,9 +1523,11 @@ its geolocation. ### param: BrowserContext.setHTTPCredentials.httpCredentials * since: v1.8 -- `httpCredentials` <[null]|[Object]> +* langs: js +- `httpCredentials` <[null]|[Object]|[Array]<[Object]>> - `username` <[string]> - `password` <[string]> + - `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one credentials entry. ## async method: BrowserContext.setOffline * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index ce5c909e6b9c1..cc8bced4ff953 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -706,6 +706,25 @@ An object containing additional HTTP headers to be sent with every request. Defa Whether to emulate network being offline. Defaults to `false`. Learn more about [network emulation](../emulation.md#offline). ## context-option-httpcredentials +* langs: js +- `httpCredentials` <[Object]|[Array]<[Object]>> + * alias: HttpCredentials + - `username` <[string]> + - `password` <[string]> + - `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one credentials entry. + - `send` ?<[HttpCredentialsSend]<"unauthorized"|"always">> This option only applies to the requests sent from corresponding [APIRequestContext] and does not affect requests sent from the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + +Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). +If no origin is specified, the username and password are sent to any servers upon unauthorized responses. + +Pass an array to supply different credentials for different origins. When the array has more than one entry, every entry must include a unique `origin`. + +:::note +WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a one-element array) when using WebKit. +::: + +## context-option-httpcredentials-csharp-java-python +* langs: csharp, java, python - `httpCredentials` <[Object]> * alias: HttpCredentials - `username` <[string]> @@ -1091,6 +1110,7 @@ between the same pixel in compared images, between zero (strict) and one (lax), - %%-context-option-extrahttpheaders-%% - %%-context-option-offline-%% - %%-context-option-httpcredentials-%% +- %%-context-option-httpcredentials-csharp-java-python-%% - %%-context-option-colorscheme-%% - %%-context-option-colorscheme-csharp-python-%% - %%-context-option-reducedMotion-%% diff --git a/docs/src/electron-api/class-electron.md b/docs/src/electron-api/class-electron.md index 60e64080e0551..23ccc8c27f0b6 100644 --- a/docs/src/electron-api/class-electron.md +++ b/docs/src/electron-api/class-electron.md @@ -135,6 +135,9 @@ Maximum time in milliseconds to wait for the application to start. Defaults to ` ### option: Electron.launch.httpcredentials = %%-context-option-httpcredentials-%% * since: v1.12 +### option: Electron.launch.httpcredentials = %%-context-option-httpcredentials-csharp-java-python-%% +* since: v1.12 + ### option: Electron.launch.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%% * since: v1.12 diff --git a/docs/src/network.md b/docs/src/network.md index 7ee76e8c71f7f..6459a8fb62ff7 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -73,6 +73,14 @@ const context = await browser.newContext({ }); const page = await context.newPage(); await page.goto('https://example.com'); + +// Different credentials for different origins: +const context2 = await browser.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: 'https://server1.com' }, + { username: 'user2', password: 'pass2', origin: 'https://server2.com' }, + ], +}); ``` ```java diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 3ab60c474dfe3..c7d0a25b13e74 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10396,7 +10396,23 @@ export interface BrowserContext { username: string; password: string; - }): Promise; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + }|ReadonlyArray<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + }>): Promise; /** * @param offline Whether to emulate network being offline for the browser context. @@ -11166,6 +11182,13 @@ export interface Browser { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -11173,7 +11196,8 @@ export interface Browser { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -11185,7 +11209,26 @@ export interface Browser { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -17192,6 +17235,13 @@ export interface BrowserType { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -17199,7 +17249,8 @@ export interface BrowserType { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -17211,7 +17262,26 @@ export interface BrowserType { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * If `true`, Playwright does not pass its own configurations args and only uses the ones from @@ -18801,6 +18871,13 @@ export interface APIRequest { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -18808,7 +18885,8 @@ export interface APIRequest { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -18820,7 +18898,26 @@ export interface APIRequest { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -23503,6 +23600,13 @@ export interface Electron { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -23510,7 +23614,8 @@ export interface Electron { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -23522,7 +23627,26 @@ export interface Electron { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -24134,6 +24258,13 @@ export interface AndroidDevice { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -24141,7 +24272,8 @@ export interface AndroidDevice { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -24153,7 +24285,26 @@ export interface AndroidDevice { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -25360,8 +25511,15 @@ export interface BrowserContextOptions { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ - httpCredentials?: HTTPCredentials; + httpCredentials?: HTTPCredentials|Array; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -25662,7 +25820,8 @@ export interface HTTPCredentials { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 82445b69f0941..9ad1eca95a4a7 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -46,7 +46,7 @@ import { Worker } from './worker'; import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; import { mkdirIfNeeded } from './fileUtils'; -import type { BrowserContextOptions, Headers, SetStorageState, StorageState, WaitForEventOptions } from './types'; +import type { BrowserContextOptions, Headers, HTTPCredentials, SetStorageState, StorageState, WaitForEventOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type { URLMatch } from '@isomorphic/urlMatch'; @@ -354,8 +354,8 @@ export class BrowserContext extends ChannelOwner await this._channel.setOffline({ offline }, kNoTimeout); } - async setHTTPCredentials(httpCredentials: { username: string, password: string } | null): Promise { - await this._channel.setHTTPCredentials({ httpCredentials: httpCredentials || undefined }, kNoTimeout); + async setHTTPCredentials(httpCredentials: HTTPCredentials | HTTPCredentials[] | null): Promise { + await this._channel.setHTTPCredentials({ httpCredentials: toHttpCredentialsProtocol(httpCredentials) }, kNoTimeout); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { @@ -561,12 +561,22 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions contrast: options.contrast === null ? 'no-override' : options.contrast, acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), + httpCredentials: toHttpCredentialsProtocol(options.httpCredentials), }; if (contextParams.recordVideo && contextParams.recordVideo.dir) contextParams.recordVideo.dir = path.resolve(contextParams.recordVideo.dir); return contextParams; } +export function toHttpCredentialsProtocol(httpCredentials?: HTTPCredentials | HTTPCredentials[] | null): channels.BrowserNewContextParams['httpCredentials'] { + if (httpCredentials === null || httpCredentials === undefined) + return undefined; + const list = Array.isArray(httpCredentials) ? httpCredentials : [httpCredentials]; + if (!list.length) + return undefined; + return list; +} + function toAcceptDownloadsProtocol(acceptDownloads?: boolean) { if (acceptDownloads === undefined) return undefined; diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index e24253c85e32f..21e2e71815996 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -409,7 +409,7 @@ export type AndroidDeviceLaunchBrowserParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -481,7 +481,7 @@ export type AndroidDeviceLaunchBrowserOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -866,7 +866,7 @@ export type BrowserNewContextParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -941,7 +941,7 @@ export type BrowserNewContextOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1019,7 +1019,7 @@ export type BrowserNewContextForReuseParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1094,7 +1094,7 @@ export type BrowserNewContextForReuseOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1211,7 +1211,7 @@ export type BrowserContextInitializer = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1486,14 +1486,14 @@ export type BrowserContextSetHTTPCredentialsParams = { username: string, password: string, origin?: string, - }, + }[], }; export type BrowserContextSetHTTPCredentialsOptions = { httpCredentials?: { username: string, password: string, origin?: string, - }, + }[], }; export type BrowserContextSetHTTPCredentialsResult = void; export type BrowserContextSetNetworkInterceptionPatternsParams = { @@ -1885,7 +1885,7 @@ export type BrowserTypeLaunchPersistentContextParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1973,7 +1973,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -2083,7 +2083,7 @@ export type ElectronLaunchParams = { username: string, password: string, origin?: string, - }, + }[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -2126,7 +2126,7 @@ export type ElectronLaunchOptions = { username: string, password: string, origin?: string, - }, + }[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -4669,7 +4669,7 @@ export type PlaywrightNewRequestParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], proxy?: { server: string, bypass?: string, @@ -4701,7 +4701,7 @@ export type PlaywrightNewRequestOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], proxy?: { server: string, bypass?: string, diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index d433427c57ba2..9eb09cdfa299c 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -25,7 +25,7 @@ import { Waiter } from './waiter'; import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; import type { Page } from './page'; -import type { BrowserContextOptions, Headers, TimeoutOptions, WaitForEventOptions } from './types'; +import type { BrowserContextOptions, Headers, HTTPCredentials, TimeoutOptions, WaitForEventOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type * as channels from './channels'; @@ -33,14 +33,14 @@ import type * as childProcess from 'child_process'; import type { BrowserWindow } from 'electron'; import type { Playwright } from './playwright'; -type ElectronOptions = Omit & { +type ElectronOptions = Omit & { env?: NodeJS.ProcessEnv, extraHTTPHeaders?: Headers, recordHar?: BrowserContextOptions['recordHar'], colorScheme?: 'dark' | 'light' | 'no-preference' | null, acceptDownloads?: boolean, - timeout?: number, -}; + httpCredentials?: HTTPCredentials | HTTPCredentials[], +} & TimeoutOptions; type ElectronAppType = typeof import('electron'); diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 73834d56e91b8..c34db2fcf921b 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -21,7 +21,7 @@ import { inspect } from 'util'; import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { isString } from '@isomorphic/rtti'; -import { toClientCertificatesProtocol } from './browserContext'; +import { toClientCertificatesProtocol, toHttpCredentialsProtocol } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { TargetClosedError, isTargetClosedError } from './errors'; import { RawHeaders } from './network'; @@ -31,7 +31,7 @@ import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; import type { Playwright } from './playwright'; import type { ResourceTiming } from './network'; -import type { ClientCertificate, FilePayload, Headers, RemoteAddr, SecurityDetails, SetStorageState, StorageState, TimeoutOptions } from './types'; +import type { ClientCertificate, FilePayload, Headers, HTTPCredentials, RemoteAddr, SecurityDetails, SetStorageState, StorageState, TimeoutOptions } from './types'; import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray, NameValue } from '@isomorphic/types'; @@ -52,10 +52,11 @@ export type FetchOptions = { maxRetries?: number, }; -export type NewContextOptions = Omit & { +export type NewContextOptions = Omit & { extraHTTPHeaders?: Headers, storageState?: string | SetStorageState, clientCertificates?: ClientCertificate[]; + httpCredentials?: HTTPCredentials | HTTPCredentials[]; }; type RequestWithBodyOptions = Omit; @@ -80,6 +81,7 @@ export class APIRequest implements api.APIRequest { storageState, tracesDir: this._playwright._defaultLaunchOptions?.tracesDir, // We do not expose tracesDir in the API, so do not allow options to accidentally override it. clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), + httpCredentials: toHttpCredentialsProtocol(options.httpCredentials), }, kNoTimeout)).request); this._contexts.add(context); context._request = this; diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index f340521105e1a..17017f11935ce 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -63,7 +63,14 @@ export type ClientCertificate = { passphrase?: string; }; -export type BrowserContextOptions = Omit & { +export type HTTPCredentials = { + username: string; + password: string; + origin?: string; + send?: 'unauthorized' | 'always'; +}; + +export type BrowserContextOptions = Omit & { viewport?: Size | null; extraHTTPHeaders?: Headers; logger?: Logger; @@ -86,6 +93,7 @@ export type BrowserContextOptions = Omit { + async doSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise { this._options.httpCredentials = httpCredentials; for (const page of this.pages()) await (page.delegate as BidiPage).updateHttpCredentials(); diff --git a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts index c413733c4b067..efe4b7a0107b7 100644 --- a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts +++ b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts @@ -16,6 +16,7 @@ import { eventsHelper } from '@utils/eventsHelper'; import { parseRawCookie } from '../cookieStore'; +import { findHttpCredentials } from '../httpCredentials'; import * as network from '../network'; import * as bidi from './third_party/bidiProtocol'; @@ -35,7 +36,7 @@ export class BidiNetworkManager { private readonly _eventListeners: RegisteredListener[]; private _userRequestInterceptionEnabled: boolean = false; private _protocolRequestInterceptionEnabled: boolean = false; - private _credentials: types.Credentials | undefined; + private _credentials: types.Credentials[] | undefined; private _attemptedAuthentications = new Set(); private _intercepId: bidi.Network.Intercept | undefined; @@ -197,8 +198,8 @@ export class BidiNetworkManager { private _onAuthRequired(params: bidi.Network.AuthRequiredParameters) { const isBasic = params.response.authChallenges?.some(challenge => challenge.scheme.startsWith('Basic')); - const credentials = this._page.browserContext._options.httpCredentials; - if (isBasic && credentials && (!credentials.origin || (new URL(params.request.url).origin).toLowerCase() === credentials.origin.toLowerCase())) { + const credentials = findHttpCredentials(this._page.browserContext._options.httpCredentials, params.request.url); + if (isBasic && credentials) { if (this._attemptedAuthentications.has(params.request.request)) { this._session.sendMayFail('network.continueWithAuth', { request: params.request.request, @@ -234,13 +235,13 @@ export class BidiNetworkManager { await this._updateProtocolRequestInterception(); } - async setCredentials(credentials: types.Credentials | undefined) { + async setCredentials(credentials: types.Credentials[] | undefined) { this._credentials = credentials; await this._updateProtocolRequestInterception(); } async _updateProtocolRequestInterception(initial?: boolean) { - const enabled = this._userRequestInterceptionEnabled || !!this._credentials; + const enabled = this._userRequestInterceptionEnabled || !!this._credentials?.length; if (enabled === this._protocolRequestInterceptionEnabled) return; this._protocolRequestInterceptionEnabled = enabled; diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 96d5c2154c3d0..a2b0e70faf886 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -24,6 +24,7 @@ import { Credentials } from './credentials'; import { Debugger } from './debugger'; import { DialogManager } from './dialog'; import { BrowserContextAPIRequestContext } from './fetch'; +import { verifyHttpCredentials } from './httpCredentials'; import { helper } from './helper'; import { EventMap, SdkObject } from './instrumentation'; import * as network from './network'; @@ -292,7 +293,7 @@ export abstract class BrowserContext extends Sdk protected abstract doClearCookies(): Promise; protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise; protected abstract doClearPermissions(): Promise; - protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise; + protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise; protected abstract doAddInitScript(initScript: InitScript): Promise; protected abstract doRemoveInitScripts(initScripts: InitScript[]): Promise; protected abstract doUpdateExtraHTTPHeaders(): Promise; @@ -348,11 +349,12 @@ export abstract class BrowserContext extends Sdk }))); } - setHTTPCredentials(progress: Progress, httpCredentials?: types.Credentials): Promise { + setHTTPCredentials(progress: Progress, httpCredentials?: types.Credentials[]): Promise { return progress.race(this.innerSetHTTPCredentials(httpCredentials)); } - innerSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + innerSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise { + verifyHttpCredentials(httpCredentials); return this.doSetHTTPCredentials(httpCredentials); } @@ -484,7 +486,7 @@ export abstract class BrowserContext extends Sdk const proxy = this._options.proxy || this._browser.options.proxy || { username: undefined, password: undefined }; const { username, password } = proxy; if (username) { - this._options.httpCredentials = { username, password: password! }; + this._options.httpCredentials = [{ username, password: password! }]; const token = Buffer.from(`${username}:${password}`).toString('base64'); this._options.extraHTTPHeaders = network.mergeHeaders([ this._options.extraHTTPHeaders, @@ -499,7 +501,7 @@ export abstract class BrowserContext extends Sdk return; const { username, password } = proxy; if (username) - this._options.httpCredentials = { username, password: password || '' }; + this._options.httpCredentials = [{ username, password: password || '' }]; } async addInitScript(progress: Progress, source: string): Promise { @@ -800,6 +802,9 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio if (options.proxy) options.proxy = normalizeProxySettings(options.proxy); verifyGeolocation(options.geolocation); + verifyHttpCredentials(options.httpCredentials); + if (browserOptions.name === 'webkit' && (options.httpCredentials?.length ?? 0) > 1) + throw new Error('Multiple httpCredentials are not supported in WebKit'); } export function verifyGeolocation(geolocation?: types.Geolocation): asserts geolocation is types.Geolocation { diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index 62c59f0116e2e..055318bc97647 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -410,7 +410,7 @@ export type AndroidDeviceLaunchBrowserParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -482,7 +482,7 @@ export type AndroidDeviceLaunchBrowserOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -867,7 +867,7 @@ export type BrowserNewContextParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -942,7 +942,7 @@ export type BrowserNewContextOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1020,7 +1020,7 @@ export type BrowserNewContextForReuseParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1095,7 +1095,7 @@ export type BrowserNewContextForReuseOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1212,7 +1212,7 @@ export type BrowserContextInitializer = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1487,14 +1487,14 @@ export type BrowserContextSetHTTPCredentialsParams = { username: string, password: string, origin?: string, - }, + }[], }; export type BrowserContextSetHTTPCredentialsOptions = { httpCredentials?: { username: string, password: string, origin?: string, - }, + }[], }; export type BrowserContextSetHTTPCredentialsResult = void; export type BrowserContextSetNetworkInterceptionPatternsParams = { @@ -1886,7 +1886,7 @@ export type BrowserTypeLaunchPersistentContextParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1974,7 +1974,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -2084,7 +2084,7 @@ export type ElectronLaunchParams = { username: string, password: string, origin?: string, - }, + }[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -2127,7 +2127,7 @@ export type ElectronLaunchOptions = { username: string, password: string, origin?: string, - }, + }[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -4670,7 +4670,7 @@ export type PlaywrightNewRequestParams = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], proxy?: { server: string, bypass?: string, @@ -4702,7 +4702,7 @@ export type PlaywrightNewRequestOptions = { password: string, origin?: string, send?: 'always' | 'unauthorized', - }, + }[], proxy?: { server: string, bypass?: string, diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index e4d95a68432d2..a85b1be67ea27 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -509,7 +509,7 @@ export class CRBrowserContext extends BrowserContext { await (sw as CRServiceWorker).updateOffline(); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise { this._options.httpCredentials = httpCredentials; for (const page of this.pages()) await (page.delegate as CRPage).updateHttpCredentials(); diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 1c720b5821004..eb439d9cb34ad 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -19,6 +19,7 @@ import { eventsHelper } from '@utils/eventsHelper'; import { assert } from '@isomorphic/assert'; import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers'; import { helper } from '../helper'; +import { findHttpCredentials } from '../httpCredentials'; import * as network from '../network'; import { isProtocolError, isSessionClosedError } from '../protocolError'; @@ -45,7 +46,7 @@ export class CRNetworkManager { private _serviceWorker: CRServiceWorker | null; private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); - private _credentials: {origin?: string, username: string, password: string} | null = null; + private _credentials: types.Credentials[] | null = null; private _attemptedAuthentications = new Set(); private _userRequestInterceptionEnabled = false; private _protocolRequestInterceptionEnabled = false; @@ -120,7 +121,7 @@ export class CRNetworkManager { })); } - async authenticate(credentials: types.Credentials | null) { + async authenticate(credentials: types.Credentials[] | null) { this._credentials = credentials; await this._updateProtocolRequestInterception(); } @@ -153,7 +154,7 @@ export class CRNetworkManager { } async _updateProtocolRequestInterception() { - const enabled = this._userRequestInterceptionEnabled || !!this._credentials; + const enabled = this._userRequestInterceptionEnabled || !!this._credentials?.length; if (enabled === this._protocolRequestInterceptionEnabled) return; this._protocolRequestInterceptionEnabled = enabled; @@ -225,26 +226,20 @@ export class CRNetworkManager { _onAuthRequired(sessionInfo: SessionInfo, event: Protocol.Fetch.authRequiredPayload) { let response: 'Default' | 'CancelAuth' | 'ProvideCredentials' = 'Default'; - const shouldProvideCredentials = this._shouldProvideCredentials(event.request.url); + const credentials = findHttpCredentials(this._credentials ?? undefined, event.request.url); if (this._attemptedAuthentications.has(event.requestId)) { response = 'CancelAuth'; - } else if (shouldProvideCredentials) { + } else if (credentials) { response = 'ProvideCredentials'; this._attemptedAuthentications.add(event.requestId); } - const { username, password } = shouldProvideCredentials && this._credentials ? this._credentials : { username: undefined, password: undefined }; + const { username, password } = credentials || { username: undefined, password: undefined }; sessionInfo.session._sendMayFail('Fetch.continueWithAuth', { requestId: event.requestId, authChallengeResponse: { response, username, password }, }); } - _shouldProvideCredentials(url: string): boolean { - if (!this._credentials) - return false; - return !this._credentials.origin || new URL(url).origin.toLowerCase() === this._credentials.origin.toLowerCase(); - } - _onRequestPaused(sessionInfo: SessionInfo, event: Protocol.Fetch.requestPausedPayload) { if (!event.networkId) { // Fetch without networkId means that request was not recognized by inspector, and diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 4a71fd9d6cf56..8cdf8a78a3851 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -32,6 +32,7 @@ import { BrowserContext, verifyClientCertificates } from './browserContext'; import { Cookie, CookieStore, domainMatches, parseRawCookie } from './cookieStore'; import { MultipartFormData } from './formData'; import { TargetClosedError } from './errors'; +import { findHttpCredentials, verifyHttpCredentials } from './httpCredentials'; import { SdkObject } from './instrumentation'; import { isAbortError } from './progress'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; @@ -43,7 +44,6 @@ import type { Playwright } from './playwright'; import type { Progress } from './progress'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; -import type { HTTPCredentials } from '../../types/types'; import type { RegisteredListener } from '@utils/eventsHelper'; import type * as channels from './channels'; import type * as har from '@trace/har'; @@ -55,7 +55,7 @@ type FetchRequestOptions = { userAgent: string; extraHTTPHeaders?: HeadersArray; failOnStatusCode?: boolean; - httpCredentials?: HTTPCredentials; + httpCredentials?: types.Credentials[]; proxy?: ProxySettings; ignoreHTTPSErrors?: boolean; maxRedirects?: number; @@ -637,9 +637,7 @@ export abstract class APIRequestContext extends SdkObject { } private _getHttpCredentials(url: URL) { - if (!this._defaultOptions().httpCredentials?.origin || url.origin.toLowerCase() === this._defaultOptions().httpCredentials?.origin?.toLowerCase()) - return this._defaultOptions().httpCredentials; - return undefined; + return findHttpCredentials(this._defaultOptions().httpCredentials, url); } } @@ -830,6 +828,7 @@ export class GlobalAPIRequestContext extends APIRequestContext { this._cookieStore.addCookies(options.storageState.cookies || []); } verifyClientCertificates(options.clientCertificates); + verifyHttpCredentials(options.httpCredentials); this._options = { baseURL: options.baseURL, userAgent: options.userAgent || getUserAgent(), @@ -973,7 +972,7 @@ function isNetworkConnectionError(e: any): boolean { return code === 'ECONNRESET' || code === 'EPIPE' || code === 'ECONNABORTED'; } -function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: HTTPCredentials) { +function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: types.Credentials) { const { username, password } = credentials; const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64'); setHeader(headers, 'authorization', `Basic ${encoded}`); diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 120e448cd419f..46f1a2ad2adfe 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -319,12 +319,12 @@ export class FFBrowserContext extends BrowserContext { await this._browser.session.send('Browser.setOnlineOverride', { browserContextId: this._browserContextId, override: this._options.offline ? 'offline' : 'online' }); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise { this._options.httpCredentials = httpCredentials; let credentials = null; - if (httpCredentials) { - const { username, password, origin } = httpCredentials; - credentials = { username, password, origin }; + if (httpCredentials?.length) { + // Keep single-credential wire format for current Firefox; send array when multiple. + credentials = httpCredentials.length === 1 ? httpCredentials[0] : httpCredentials; } await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials }); } diff --git a/packages/playwright-core/src/server/firefox/protocol.d.ts b/packages/playwright-core/src/server/firefox/protocol.d.ts index e55a72ce404b7..370b2bc6704a4 100644 --- a/packages/playwright-core/src/server/firefox/protocol.d.ts +++ b/packages/playwright-core/src/server/firefox/protocol.d.ts @@ -136,7 +136,11 @@ export namespace Protocol { username: string; password: string; origin?: string; - }|null; + }|{ + username: string; + password: string; + origin?: string; + }[]|null; }; export type setHTTPCredentialsReturnValue = void; export type setRequestInterceptionParameters = { diff --git a/packages/playwright-core/src/server/httpCredentials.ts b/packages/playwright-core/src/server/httpCredentials.ts new file mode 100644 index 0000000000000..f2077a11b584c --- /dev/null +++ b/packages/playwright-core/src/server/httpCredentials.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +import type * as types from './types'; + +export function verifyHttpCredentials(httpCredentials: types.Credentials[] | undefined) { + if (!httpCredentials) + return; + const origins = new Set(); + for (const credentials of httpCredentials) { + if (httpCredentials.length > 1 && !credentials.origin) + throw new Error('httpCredentials.origin is required when providing multiple credentials'); + if (!credentials.origin) + continue; + let normalized: string; + try { + normalized = normalizeHttpOrigin(credentials.origin); + } catch (e) { + throw new Error(`httpCredentials.origin: ${String(e)}`); + } + if (origins.has(normalized)) + throw new Error(`httpCredentials: duplicate origin "${credentials.origin}"`); + origins.add(normalized); + } +} + +export function normalizeHttpOrigin(origin: string): string { + return new URL(origin).origin.toLowerCase(); +} + +export function findHttpCredentials(httpCredentials: types.Credentials[] | undefined, url: string | URL): types.Credentials | undefined { + if (!httpCredentials?.length) + return undefined; + const requestOrigin = (typeof url === 'string' ? new URL(url) : url).origin.toLowerCase(); + const matching = httpCredentials.find(credentials => credentials.origin && normalizeHttpOrigin(credentials.origin) === requestOrigin); + if (matching) + return matching; + if (httpCredentials.length === 1 && !httpCredentials[0].origin) + return httpCredentials[0]; + return undefined; +} diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index a834844c09036..40b9427f14975 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -62,7 +62,7 @@ export type Credentials = { username: string; password: string; origin?: string; - sendImmediately?: boolean; + send?: 'always' | 'unauthorized'; }; export type Geolocation = { diff --git a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts index 00907301fae8d..89ce1e350c478 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts @@ -415,6 +415,6 @@ export class WVBrowserContext extends BrowserContext { override async clearCache(): Promise { throw new Error('Method not implemented.'); } override async doClose(reason: string | undefined): Promise { throw new Error('Method not implemented.'); } override async cancelDownload(uuid: string) { throw new Error('Method not implemented.'); } - protected override async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { throw new Error('Method not implemented.'); } + protected override async doSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise { throw new Error('Method not implemented.'); } protected override async doUpdateOffline(): Promise { throw new Error('Method not implemented.'); } } diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 12fbb53f99c12..0559bea5e2d8d 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -324,7 +324,9 @@ export class WKBrowserContext extends BrowserContext { await (page.delegate as WKPage).updateOffline(); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: types.Credentials[]): Promise { + if ((httpCredentials?.length ?? 0) > 1) + throw new Error('Multiple httpCredentials are not supported in WebKit'); this._options.httpCredentials = httpCredentials; for (const page of this.pages()) await (page.delegate as WKPage).updateHttpCredentials(); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index ef084aed771e6..48e177b3564a5 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -755,7 +755,7 @@ export class WKPage implements PageDelegate { } async updateHttpCredentials() { - const credentials = this._browserContext._options.httpCredentials || { username: '', password: '', origin: '' }; + const credentials = this._browserContext._options.httpCredentials?.[0] || { username: '', password: '', origin: '' }; await this._pageProxySession.send('Emulation.setAuthCredentials', { username: credentials.username, password: credentials.password, origin: credentials.origin }); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 3ab60c474dfe3..c7d0a25b13e74 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10396,7 +10396,23 @@ export interface BrowserContext { username: string; password: string; - }): Promise; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + }|ReadonlyArray<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + }>): Promise; /** * @param offline Whether to emulate network being offline for the browser context. @@ -11166,6 +11182,13 @@ export interface Browser { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -11173,7 +11196,8 @@ export interface Browser { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -11185,7 +11209,26 @@ export interface Browser { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -17192,6 +17235,13 @@ export interface BrowserType { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -17199,7 +17249,8 @@ export interface BrowserType { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -17211,7 +17262,26 @@ export interface BrowserType { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * If `true`, Playwright does not pass its own configurations args and only uses the ones from @@ -18801,6 +18871,13 @@ export interface APIRequest { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -18808,7 +18885,8 @@ export interface APIRequest { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -18820,7 +18898,26 @@ export interface APIRequest { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -23503,6 +23600,13 @@ export interface Electron { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -23510,7 +23614,8 @@ export interface Electron { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -23522,7 +23627,26 @@ export interface Electron { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -24134,6 +24258,13 @@ export interface AndroidDevice { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ httpCredentials?: { username: string; @@ -24141,7 +24272,8 @@ export interface AndroidDevice { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; @@ -24153,7 +24285,26 @@ export interface AndroidDevice { * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. */ send?: "unauthorized"|"always"; - }; + }|Array<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. + */ + origin?: string; + + /** + * This option only applies to the requests sent from corresponding + * [APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext) and does not affect requests sent from + * the browser. `'always'` - `Authorization` header with basic authentication credentials will be sent with the each + * API request. `'unauthorized` - the credentials are only sent when 401 (Unauthorized) response with + * `WWW-Authenticate` header is received. Defaults to `'unauthorized'`. + */ + send?: "unauthorized"|"always"; + }>; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -25360,8 +25511,15 @@ export interface BrowserContextOptions { /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. + * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * */ - httpCredentials?: HTTPCredentials; + httpCredentials?: HTTPCredentials|Array; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. @@ -25662,7 +25820,8 @@ export interface HTTPCredentials { password: string; /** - * Restrain sending http credentials on specific origin (scheme://host:port). + * Restrain sending http credentials on specific origin (scheme://host:port). Required when providing more than one + * credentials entry. */ origin?: string; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index d318379841738..8892aa0a70720 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -7262,6 +7262,12 @@ export interface PlaywrightTestOptions { * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). If no * origin is specified, the username and password are sent to any servers upon unauthorized responses. * + * Pass an array to supply different credentials for different origins. When the array has more than one entry, every + * entry must include a unique `origin`. + * + * **NOTE** WebKit does not support multiple `httpCredentials` entries. Pass a single credentials object (or a + * one-element array) when using WebKit. + * * **Usage** * * ```js @@ -7279,7 +7285,7 @@ export interface PlaywrightTestOptions { * ``` * */ - httpCredentials: HTTPCredentials | undefined; + httpCredentials: HTTPCredentials | HTTPCredentials[] | undefined; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. * diff --git a/packages/protocol/spec/browserContext.yml b/packages/protocol/spec/browserContext.yml index 753e9d6a2c225..a4e8207ebd38d 100644 --- a/packages/protocol/spec/browserContext.yml +++ b/packages/protocol/spec/browserContext.yml @@ -135,11 +135,13 @@ BrowserContext: group: configuration parameters: httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? + type: array? + items: + type: object + properties: + username: string + password: string + origin: string? setNetworkInterceptionPatterns: title: Route requests diff --git a/packages/protocol/spec/electron.yml b/packages/protocol/spec/electron.yml index 877b4077469af..4654172e79417 100644 --- a/packages/protocol/spec/electron.yml +++ b/packages/protocol/spec/electron.yml @@ -52,11 +52,13 @@ Electron: latitude: float accuracy: float? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? + type: array? + items: + type: object + properties: + username: string + password: string + origin: string? ignoreHTTPSErrors: boolean? locale: string? offline: boolean? diff --git a/packages/protocol/spec/mixins.yml b/packages/protocol/spec/mixins.yml index 36c197654b6ca..77bca1220ebfb 100644 --- a/packages/protocol/spec/mixins.yml +++ b/packages/protocol/spec/mixins.yml @@ -138,16 +138,18 @@ ContextOptions: items: NameValue offline: boolean? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized + type: array? + items: + type: object + properties: + username: string + password: string + origin: string? + send: + type: enum? + literals: + - always + - unauthorized deviceScaleFactor: float? isMobile: boolean? hasTouch: boolean? diff --git a/packages/protocol/spec/playwright.yml b/packages/protocol/spec/playwright.yml index 287f3714d4076..f3496154c00e9 100644 --- a/packages/protocol/spec/playwright.yml +++ b/packages/protocol/spec/playwright.yml @@ -64,16 +64,18 @@ Playwright: pfx: binary? maxRedirects: int? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized + type: array? + items: + type: object + properties: + username: string + password: string + origin: string? + send: + type: enum? + literals: + - always + - unauthorized proxy: type: object? properties: diff --git a/packages/protocol/src/validator.ts b/packages/protocol/src/validator.ts index d18c0b9138ef0..06201a8189c6a 100644 --- a/packages/protocol/src/validator.ts +++ b/packages/protocol/src/validator.ts @@ -172,12 +172,12 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), send: tOptional(tEnum(['always', 'unauthorized'])), - })), + }))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -458,12 +458,12 @@ scheme.BrowserNewContextParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), send: tOptional(tEnum(['always', 'unauthorized'])), - })), + }))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -536,12 +536,12 @@ scheme.BrowserNewContextForReuseParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), send: tOptional(tEnum(['always', 'unauthorized'])), - })), + }))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -636,12 +636,12 @@ scheme.BrowserContextInitializer = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), send: tOptional(tEnum(['always', 'unauthorized'])), - })), + }))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -807,11 +807,11 @@ scheme.BrowserContextSetGeolocationParams = tObject({ }); scheme.BrowserContextSetGeolocationResult = tOptional(tObject({})); scheme.BrowserContextSetHTTPCredentialsParams = tObject({ - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), - })), + }))), }); scheme.BrowserContextSetHTTPCredentialsResult = tOptional(tObject({})); scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({ @@ -1051,12 +1051,12 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), send: tOptional(tEnum(['always', 'unauthorized'])), - })), + }))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -1151,11 +1151,11 @@ scheme.ElectronLaunchParams = tObject({ latitude: tFloat, accuracy: tOptional(tFloat), })), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), - })), + }))), ignoreHTTPSErrors: tOptional(tBoolean), locale: tOptional(tString), offline: tOptional(tBoolean), @@ -2694,12 +2694,12 @@ scheme.PlaywrightNewRequestParams = tObject({ pfx: tOptional(tBinary), }))), maxRedirects: tOptional(tInt), - httpCredentials: tOptional(tObject({ + httpCredentials: tOptional(tArray(tObject({ username: tString, password: tString, origin: tOptional(tString), send: tOptional(tEnum(['always', 'unauthorized'])), - })), + }))), proxy: tOptional(tObject({ server: tString, bypass: tOptional(tString), diff --git a/tests/library/browsercontext-credentials.spec.ts b/tests/library/browsercontext-credentials.spec.ts index 20e285c568252..b25a7d30677ce 100644 --- a/tests/library/browsercontext-credentials.spec.ts +++ b/tests/library/browsercontext-credentials.spec.ts @@ -152,3 +152,76 @@ it('should fail with correct credentials and mismatching port', async ({ browser expect(responseOrError.status()).toBe(401); await context.close(); }); + +it('should work with multiple credentials for different origins', async ({ browser, server, browserName }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22013' }); + it.skip(browserName === 'webkit', 'Multiple httpCredentials are not supported in WebKit'); + // Firefox multi-credentials require a browser roll with updated juggler; keep single-object BC working. + it.skip(browserName === 'firefox', 'Requires Firefox with multi-credentials juggler support'); + + server.setAuth('/one.html', 'user1', 'pass1'); + server.setAuth('/two.html', 'user2', 'pass2'); + server.setRoute('/one.html', (req, res) => { + res.end('one'); + }); + server.setRoute('/two.html', (req, res) => { + res.end('two'); + }); + + const context = await browser.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + const page = await context.newPage(); + expect((await page.goto(server.PREFIX + '/one.html'))!.status()).toBe(200); + expect(await page.content()).toContain('one'); + expect((await page.goto(server.CROSS_PROCESS_PREFIX + '/two.html'))!.status()).toBe(200); + expect(await page.content()).toContain('two'); + // Wrong credentials for this origin. + expect((await page.goto(server.PREFIX + '/two.html'))!.status()).toBe(401); + expect((await page.goto(server.CROSS_PROCESS_PREFIX + '/one.html'))!.status()).toBe(401); + await context.close(); +}); + +it('should work with a one-element credentials array', async ({ browser, server }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: [{ username: 'user', password: 'pass' }] + }); + const page = await context.newPage(); + expect((await page.goto(server.EMPTY_PAGE))!.status()).toBe(200); + await context.close(); +}); + +it('should throw when multiple credentials are missing origin', async ({ browser }) => { + const error = await browser.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1' }, + { username: 'user2', password: 'pass2', origin: 'https://example.com' }, + ] + }).catch(e => e); + expect(error.message).toContain('httpCredentials.origin is required when providing multiple credentials'); +}); + +it('should throw when multiple credentials have duplicate origins', async ({ browser, server }) => { + const error = await browser.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.PREFIX.toUpperCase() }, + ] + }).catch(e => e); + expect(error.message).toContain('duplicate origin'); +}); + +it('should throw for multiple credentials in WebKit', async ({ browser, browserName, server }) => { + it.skip(browserName !== 'webkit'); + const error = await browser.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }).catch(e => e); + expect(error.message).toContain('Multiple httpCredentials are not supported in WebKit'); +}); diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 46831e62ca49e..da22a24c4967a 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -587,6 +587,24 @@ it('should support HTTPCredentials.send for newContext', async ({ contextFactory } }); +it('should support multiple httpCredentials for context.request', async ({ contextFactory, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22013' }); + server.setAuth('/one.html', 'user1', 'pass1'); + server.setAuth('/two.html', 'user2', 'pass2'); + server.setRoute('/one.html', (req, res) => res.end('one')); + server.setRoute('/two.html', (req, res) => res.end('two')); + const context = await contextFactory({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + expect((await context.request.get(server.PREFIX + '/one.html')).status()).toBe(200); + expect((await context.request.get(server.CROSS_PROCESS_PREFIX + '/two.html')).status()).toBe(200); + expect((await context.request.get(server.PREFIX + '/two.html')).status()).toBe(401); + expect((await context.request.get(server.CROSS_PROCESS_PREFIX + '/one.html')).status()).toBe(401); +}); + it('should support HTTPCredentials.send for browser.newPage', async ({ contextFactory, server, browser }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' }); const page = await browser.newPage({ diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts index 457d8c2e1f86f..07d44311feb6b 100644 --- a/tests/library/global-fetch.spec.ts +++ b/tests/library/global-fetch.spec.ts @@ -219,6 +219,53 @@ it('should support HTTPCredentials.send', async ({ playwright, server }) => { await request.dispose(); }); +it('should support multiple httpCredentials for different origins', async ({ playwright, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22013' }); + server.setAuth('/one.html', 'user1', 'pass1'); + server.setAuth('/two.html', 'user2', 'pass2'); + server.setRoute('/one.html', (req, res) => res.end('one')); + server.setRoute('/two.html', (req, res) => res.end('two')); + + const request = await playwright.request.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + expect((await request.get(server.PREFIX + '/one.html')).status()).toBe(200); + expect((await request.get(server.CROSS_PROCESS_PREFIX + '/two.html')).status()).toBe(200); + expect((await request.get(server.PREFIX + '/two.html')).status()).toBe(401); + expect((await request.get(server.CROSS_PROCESS_PREFIX + '/one.html')).status()).toBe(401); + await request.dispose(); +}); + +it('should support multiple httpCredentials with send always', async ({ playwright, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22013' }); + const request = await playwright.request.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX, send: 'always' }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX, send: 'always' }, + ] + }); + { + const [serverRequest, response] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user1:pass1').toString('base64')); + expect(response.status()).toBe(200); + } + { + const [serverRequest, response] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.CROSS_PROCESS_PREFIX + '/empty.html') + ]); + expect(serverRequest.headers.authorization).toBe('Basic ' + Buffer.from('user2:pass2').toString('base64')); + expect(response.status()).toBe(200); + } + await request.dispose(); +}); + it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => { const request = await playwright.request.newContext({ ignoreHTTPSErrors: true }); const response = await request.get(httpsServer.EMPTY_PAGE); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index f06954a448433..e9ecfa850794b 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -278,7 +278,7 @@ export interface PlaywrightTestOptions { extraHTTPHeaders: ExtraHTTPHeaders | undefined; geolocation: Geolocation | undefined; hasTouch: boolean; - httpCredentials: HTTPCredentials | undefined; + httpCredentials: HTTPCredentials | HTTPCredentials[] | undefined; ignoreHTTPSErrors: boolean; isMobile: boolean; javaScriptEnabled: boolean;