From 7383851ed3183382dff5a619fd974f600e32c08d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 24 Jul 2026 15:19:45 +0100 Subject: [PATCH] feat: support multiple HTTP credentials The httpCredentials option now also accepts an array of credentials. The first entry matching the request origin is used, entries without an origin match any request. Fixes: https://github.com/microsoft/playwright/issues/22013 --- docs/src/api/class-browsercontext.md | 5 +- docs/src/api/params.md | 4 +- packages/playwright-client/types/types.d.ts | 139 +++++++++++++++++- .../src/client/browserContext.ts | 13 +- .../playwright-core/src/client/channels.d.ts | 105 +++---------- .../playwright-core/src/client/electron.ts | 3 +- packages/playwright-core/src/client/fetch.ts | 7 +- packages/playwright-core/src/client/types.ts | 4 +- .../src/server/bidi/bidiBrowser.ts | 3 +- .../src/server/bidi/bidiNetworkManager.ts | 12 +- .../src/server/browserContext.ts | 16 +- .../playwright-core/src/server/channels.d.ts | 105 +++---------- .../src/server/chromium/crBrowser.ts | 3 +- .../src/server/chromium/crNetworkManager.ts | 20 +-- packages/playwright-core/src/server/fetch.ts | 12 +- .../src/server/firefox/ffBrowser.ts | 9 +- packages/playwright-core/src/server/types.ts | 7 - .../src/server/webkit/webview/wvBrowser.ts | 3 +- .../src/server/webkit/wkBrowser.ts | 3 +- .../src/server/webkit/wkPage.ts | 4 +- packages/playwright-core/types/types.d.ts | 139 +++++++++++++++++- packages/playwright/types/test.d.ts | 5 +- packages/protocol/spec/browserContext.yml | 7 +- packages/protocol/spec/electron.yml | 7 +- packages/protocol/spec/mixins.yml | 24 +-- packages/protocol/spec/playwright.yml | 12 +- packages/protocol/src/structs.d.ts | 7 + packages/protocol/src/validator.ts | 60 ++------ .../browsercontext-credentials.spec.ts | 75 ++++++++++ tests/library/browsercontext-fetch.spec.ts | 15 ++ tests/library/global-fetch.spec.ts | 43 ++++++ utils/doclint/documentation.js | 3 + utils/generate_types/overrides-test.d.ts | 2 +- 33 files changed, 550 insertions(+), 326 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index f5c2cbc95ef12..68f0c531b3e3c 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1520,9 +1520,12 @@ its geolocation. ### param: BrowserContext.setHTTPCredentials.httpCredentials * since: v1.8 -- `httpCredentials` <[null]|[Object]> +- `httpCredentials` <[null]|[Object]|[Array]<[Object]>> - `username` <[string]> - `password` <[string]> + - `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port). + +Pass an array to use different credentials for different origins. The first entry that matches the request origin is used, and entries with no origin match any request. ## async method: BrowserContext.setOffline * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 47fe73082ada9..bf7a70b9074db 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -718,7 +718,7 @@ 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 -- `httpCredentials` <[Object]> +- `httpCredentials` <[Object]|[Array]<[Object]>> * alias: HttpCredentials - `username` <[string]> - `password` <[string]> @@ -728,6 +728,8 @@ Whether to emulate network being offline. Defaults to `false`. Learn more about 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 use different credentials for different origins. The first entry that matches the request origin is used, and entries with no origin match any request. + ## context-option-colorscheme * langs: js, java - `colorScheme` > diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index eaa024a641d57..baa5f1d759c4a 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -10453,13 +10453,28 @@ export interface BrowserContext { /** * @deprecated Browsers may cache credentials after successful authentication. Create a new browser context instead. - * @param httpCredentials + * @param httpCredentials Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ setHTTPCredentials(httpCredentials: null|{ username: string; password: string; - }): Promise; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }|ReadonlyArray<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }>): Promise; /** * @param offline Whether to emulate network being offline for the browser context. @@ -11229,6 +11244,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -11248,7 +11266,25 @@ 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). + */ + 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`. @@ -17270,6 +17306,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -17289,7 +17328,25 @@ 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). + */ + 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 @@ -18885,6 +18942,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -18904,7 +18964,25 @@ 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). + */ + 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`. @@ -23588,6 +23666,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -23607,7 +23688,25 @@ 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). + */ + 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`. @@ -24225,6 +24324,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -24244,7 +24346,25 @@ 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). + */ + 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`. @@ -25457,8 +25577,11 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ - httpCredentials?: HTTPCredentials; + httpCredentials?: HTTPCredentials|Array; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 3f1ce5e3d63b5..c971f0739048f 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -49,6 +49,7 @@ import { mkdirIfNeeded } from './fileUtils'; import type { EvaluateOptions } from './jsHandle'; import type { BrowserContextOptions, Headers, SetStorageState, StorageState, WaitForEventOptions } from './types'; +import type { HttpCredentials } from '@protocol/structs'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type { URLMatch } from '@isomorphic/urlMatch'; @@ -353,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 || undefined) }, kNoTimeout); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any, options?: EvaluateOptions) { @@ -567,12 +568,20 @@ 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(credentials?: HttpCredentials | HttpCredentials[]): HttpCredentials[] | undefined { + if (!credentials) + return undefined; + const list = Array.isArray(credentials) ? credentials : [credentials]; + return list.length ? list : undefined; +} + 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 d62e20c022afd..be860f3deb3ec 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -15,8 +15,8 @@ */ // This file is generated by generate_channels.js, do not edit manually. -import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; -export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; export type TimeoutOptions = { signal: AbortSignal | undefined, timeout: number }; @@ -404,12 +404,7 @@ export type AndroidDeviceLaunchBrowserParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -476,12 +471,7 @@ export type AndroidDeviceLaunchBrowserOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -861,12 +851,7 @@ export type BrowserNewContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -936,12 +921,7 @@ export type BrowserNewContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1014,12 +994,7 @@ export type BrowserNewContextForReuseParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1089,12 +1064,7 @@ export type BrowserNewContextForReuseOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1206,12 +1176,7 @@ export type BrowserContextInitializer = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1481,18 +1446,10 @@ export type BrowserContextSetGeolocationOptions = { }; export type BrowserContextSetGeolocationResult = void; export type BrowserContextSetHTTPCredentialsParams = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsOptions = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsResult = void; export type BrowserContextSetNetworkInterceptionPatternsParams = { @@ -1857,12 +1814,7 @@ export type BrowserTypeLaunchPersistentContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1945,12 +1897,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -2056,11 +2003,7 @@ export type ElectronLaunchParams = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -2099,11 +2042,7 @@ export type ElectronLaunchOptions = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -4651,12 +4590,7 @@ export type PlaywrightNewRequestParams = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], proxy?: { server: string, bypass?: string, @@ -4683,12 +4617,7 @@ export type PlaywrightNewRequestOptions = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], 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..927c8a4b9d1ad 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -33,12 +33,13 @@ 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, + httpCredentials?: BrowserContextOptions['httpCredentials'], timeout?: number, }; diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 73834d56e91b8..54b17863a1745 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'; @@ -32,6 +32,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 { HttpCredentials } from '@protocol/structs'; import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray, NameValue } from '@isomorphic/types'; @@ -52,10 +53,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 +82,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..81c562a1be535 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -16,6 +16,7 @@ */ import type { Size } from '@isomorphic/types'; +import type { HttpCredentials } from '@protocol/structs'; import type * as channels from './channels'; export type { HeadersArray, Point, Quad, Rect, Size } from '@isomorphic/types'; @@ -63,9 +64,10 @@ export type ClientCertificate = { passphrase?: string; }; -export type BrowserContextOptions = Omit & { +export type BrowserContextOptions = Omit & { viewport?: Size | null; extraHTTPHeaders?: Headers; + httpCredentials?: HttpCredentials | HttpCredentials[]; logger?: Logger; storageState?: string | SetStorageState; har?: { diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index cf6699c924c6f..95546d70fe53c 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -31,6 +31,7 @@ import type { SdkObject } from '../instrumentation'; import type { InitScript, Page } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { BidiSession } from './bidiConnection'; import type * as channels from '../channels'; @@ -409,7 +410,7 @@ export class BidiBrowserContext extends BrowserContext { }); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): 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..b78b485331e70 100644 --- a/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts +++ b/packages/playwright-core/src/server/bidi/bidiNetworkManager.ts @@ -15,6 +15,7 @@ */ import { eventsHelper } from '@utils/eventsHelper'; +import { findMatchingHttpCredentials } from '../browserContext'; import { parseRawCookie } from '../cookieStore'; import * as network from '../network'; import * as bidi from './third_party/bidiProtocol'; @@ -23,6 +24,7 @@ import type { RegisteredListener } from '@utils/eventsHelper'; import type * as frames from '../frames'; import type { Page } from '../page'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { BidiSession } from './bidiConnection'; import type { BidiPage } from './bidiPage'; @@ -35,7 +37,7 @@ export class BidiNetworkManager { private readonly _eventListeners: RegisteredListener[]; private _userRequestInterceptionEnabled: boolean = false; private _protocolRequestInterceptionEnabled: boolean = false; - private _credentials: types.Credentials | undefined; + private _credentials: HttpCredentials[] | undefined; private _attemptedAuthentications = new Set(); private _intercepId: bidi.Network.Intercept | undefined; @@ -197,8 +199,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 = findMatchingHttpCredentials(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 +236,13 @@ export class BidiNetworkManager { await this._updateProtocolRequestInterception(); } - async setCredentials(credentials: types.Credentials | undefined) { + async setCredentials(credentials: HttpCredentials[] | 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 04f888bfa5afa..ff4528c3f1c2e 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -45,6 +45,7 @@ import type { ClientCertificatesProxy } from './socksClientCertificatesIntercept import type { SerializedStorage } from '@injected/storageScript'; import type * as types from './types'; import type * as channels from './channels'; +import type { HttpCredentials } from '@protocol/structs'; const BrowserContextEvent = { Console: 'console', @@ -288,7 +289,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?: HttpCredentials[]): Promise; protected abstract doAddInitScript(initScript: InitScript): Promise; protected abstract doRemoveInitScripts(initScripts: InitScript[]): Promise; protected abstract doUpdateExtraHTTPHeaders(): Promise; @@ -343,11 +344,11 @@ export abstract class BrowserContext extends Sdk }))); } - setHTTPCredentials(progress: Progress, httpCredentials?: types.Credentials): Promise { + setHTTPCredentials(progress: Progress, httpCredentials?: HttpCredentials[]): Promise { return progress.race(this.innerSetHTTPCredentials(httpCredentials)); } - innerSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + innerSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { return this.doSetHTTPCredentials(httpCredentials); } @@ -479,7 +480,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, @@ -494,7 +495,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 { @@ -768,6 +769,11 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio verifyGeolocation(options.geolocation); } +export function findMatchingHttpCredentials(credentials: HttpCredentials[] | undefined, url: string): HttpCredentials | undefined { + const origin = new URL(url).origin.toLowerCase(); + return credentials?.find(c => !c.origin || c.origin.toLowerCase() === origin); +} + export function verifyGeolocation(geolocation?: types.Geolocation): asserts geolocation is types.Geolocation { if (!geolocation) return; diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index 07a6414f49295..effc7d3986ccc 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -18,8 +18,8 @@ import type { Progress } from './progress'; -import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; -export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +import type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; +export type { Binary, Channel, AndroidWebView, AndroidSelector, AndroidElementInfo, APIResponse, Metadata, ClientSideCallMetadata, SDKLanguage, WaitInfo, HttpCredentials, SetNetworkCookie, NetworkCookie, ResourceTiming, SecurityDetails, RequestSizes, RemoteAddr, ExpectedTextValue, SelectorEngine, FormField, LifecycleEvent, ConsoleMessagesFilter, RecorderSource, IndexedDBDatabase, SetOriginStorage, OriginStorage, RecordHarOptions, SerializedValue, SerializedArgument, SerializedError, StackFrame, VirtualCredential, Point, Rect, URLPattern, NameValue } from '@protocol/structs'; // ----------- Initializer Traits ----------- export type InitializerTraits = @@ -405,12 +405,7 @@ export type AndroidDeviceLaunchBrowserParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -477,12 +472,7 @@ export type AndroidDeviceLaunchBrowserOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -862,12 +852,7 @@ export type BrowserNewContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -937,12 +922,7 @@ export type BrowserNewContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1015,12 +995,7 @@ export type BrowserNewContextForReuseParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1090,12 +1065,7 @@ export type BrowserNewContextForReuseOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1207,12 +1177,7 @@ export type BrowserContextInitializer = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1482,18 +1447,10 @@ export type BrowserContextSetGeolocationOptions = { }; export type BrowserContextSetGeolocationResult = void; export type BrowserContextSetHTTPCredentialsParams = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsOptions = { - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], }; export type BrowserContextSetHTTPCredentialsResult = void; export type BrowserContextSetNetworkInterceptionPatternsParams = { @@ -1858,12 +1815,7 @@ export type BrowserTypeLaunchPersistentContextParams = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -1946,12 +1898,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { permissions?: string[], extraHTTPHeaders?: NameValue[], offline?: boolean, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], deviceScaleFactor?: number, isMobile?: boolean, hasTouch?: boolean, @@ -2057,11 +2004,7 @@ export type ElectronLaunchParams = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -2100,11 +2043,7 @@ export type ElectronLaunchOptions = { latitude: number, accuracy?: number, }, - httpCredentials?: { - username: string, - password: string, - origin?: string, - }, + httpCredentials?: HttpCredentials[], ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, @@ -4652,12 +4591,7 @@ export type PlaywrightNewRequestParams = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], proxy?: { server: string, bypass?: string, @@ -4684,12 +4618,7 @@ export type PlaywrightNewRequestOptions = { pfx?: Binary, }[], maxRedirects?: number, - httpCredentials?: { - username: string, - password: string, - origin?: string, - send?: 'always' | 'unauthorized', - }, + httpCredentials?: HttpCredentials[], 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 ba563a4b56118..db00e29a01059 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -33,6 +33,7 @@ import { CRServiceWorker } from './crServiceWorker'; import type { InitScript } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { CDPSession, CRSession } from './crConnection'; import type { CRDevTools } from './crDevTools'; import type { Protocol } from './protocol'; @@ -509,7 +510,7 @@ export class CRBrowserContext extends BrowserContext { await (sw as CRServiceWorker).updateOffline(); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): 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..de95ef8dd3d6f 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -18,6 +18,7 @@ import { eventsHelper } from '@utils/eventsHelper'; import { assert } from '@isomorphic/assert'; import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers'; +import { findMatchingHttpCredentials } from '../browserContext'; import { helper } from '../helper'; import * as network from '../network'; import { isProtocolError, isSessionClosedError } from '../protocolError'; @@ -29,6 +30,7 @@ import type * as contexts from '../browserContext'; import type * as frames from '../frames'; import type { Page } from '../page'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { CRPage } from './crPage'; import type { CRServiceWorker } from './crServiceWorker'; @@ -45,7 +47,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: HttpCredentials[] | null = null; private _attemptedAuthentications = new Set(); private _userRequestInterceptionEnabled = false; private _protocolRequestInterceptionEnabled = false; @@ -120,7 +122,7 @@ export class CRNetworkManager { })); } - async authenticate(credentials: types.Credentials | null) { + async authenticate(credentials: HttpCredentials[] | null) { this._credentials = credentials; await this._updateProtocolRequestInterception(); } @@ -153,7 +155,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 +227,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 = findMatchingHttpCredentials(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 2d32cada41a67..b3f2f520a9c00 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -28,7 +28,7 @@ import { eventsHelper } from '@utils/eventsHelper'; import { monotonicTime } from '@isomorphic/time'; import { createProxyAgent } from '@utils/network'; import { getUserAgent } from './userAgent'; -import { BrowserContext, verifyClientCertificates } from './browserContext'; +import { BrowserContext, findMatchingHttpCredentials, verifyClientCertificates } from './browserContext'; import { Cookie, CookieStore, domainMatches, parseRawCookie } from './cookieStore'; import { MultipartFormData } from './formData'; import { TargetClosedError } from './errors'; @@ -43,7 +43,7 @@ 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 { HttpCredentials } from '@protocol/structs'; 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?: HttpCredentials[]; proxy?: ProxySettings; ignoreHTTPSErrors?: boolean; maxRedirects?: number; @@ -632,9 +632,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 findMatchingHttpCredentials(this._defaultOptions().httpCredentials, url.toString()); } } @@ -841,7 +839,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: HttpCredentials) { 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 c02dad2086a79..cf2f84efb89c9 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -28,6 +28,7 @@ import type { SdkObject } from '../instrumentation'; import type { InitScript, Page } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { FFSession } from './ffConnection'; import type { Protocol } from './protocol'; import type * as channels from '../channels'; @@ -317,13 +318,9 @@ 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?: HttpCredentials[]): Promise { this._options.httpCredentials = httpCredentials; - let credentials = null; - if (httpCredentials) { - const { username, password, origin } = httpCredentials; - credentials = [{ username, password, origin }]; - } + const credentials = httpCredentials ? httpCredentials.map(({ username, password, origin }) => ({ username, password, origin })) : null; await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials }); } diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index a834844c09036..52b10d200f5f2 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -58,13 +58,6 @@ export type ScreencastFrame = { viewportHeight: number, }; -export type Credentials = { - username: string; - password: string; - origin?: string; - sendImmediately?: boolean; -}; - export type Geolocation = { longitude: number; latitude: number; diff --git a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts index 77b7f89d98a3d..0e4f37808f245 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts @@ -39,6 +39,7 @@ import type { InitScript, Page } from '../../page'; import type { Protocol } from './protocol'; import type { ProtocolRequest, ProtocolResponse } from '../../transport'; import type * as types from '../../types'; +import type { HttpCredentials } from '@protocol/structs'; import type * as channels from '../../channels'; import type { Progress } from '../../progress'; import type { ConnectOverCDPTransport } from '../../../../types/types.d.ts'; @@ -411,6 +412,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?: HttpCredentials[]): 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 d315e806c9abd..18b2da2ab203f 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -28,6 +28,7 @@ import type { SdkObject } from '../instrumentation'; import type { InitScript, Page } from '../page'; import type { ConnectionTransport } from '../transport'; import type * as types from '../types'; +import type { HttpCredentials } from '@protocol/structs'; import type { Protocol } from './protocol'; import type { PageProxyMessageReceivedPayload } from './wkConnection'; import type * as channels from '../channels'; @@ -324,7 +325,7 @@ export class WKBrowserContext extends BrowserContext { await (page.delegate as WKPage).updateOffline(); } - async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise { + async doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { 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 0ef1768254d64..8e6dc38672381 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -752,8 +752,8 @@ export class WKPage implements PageDelegate { } async updateHttpCredentials() { - const credentials = this._browserContext._options.httpCredentials || { username: '', password: '', origin: '' }; - await this._pageProxySession.send('Emulation.setAuthCredentials', { credentials: [{ username: credentials.username, password: credentials.password, origin: credentials.origin }] }); + const credentials = (this._browserContext._options.httpCredentials || []).map(c => ({ username: c.username, password: c.password, origin: c.origin })); + await this._pageProxySession.send('Emulation.setAuthCredentials', { credentials }); } async updateFileChooserInterception() { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index eaa024a641d57..baa5f1d759c4a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10453,13 +10453,28 @@ export interface BrowserContext { /** * @deprecated Browsers may cache credentials after successful authentication. Create a new browser context instead. - * @param httpCredentials + * @param httpCredentials Pass an array to use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ setHTTPCredentials(httpCredentials: null|{ username: string; password: string; - }): Promise; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }|ReadonlyArray<{ + username: string; + + password: string; + + /** + * Restrain sending http credentials on specific origin (scheme://host:port). + */ + origin?: string; + }>): Promise; /** * @param offline Whether to emulate network being offline for the browser context. @@ -11229,6 +11244,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -11248,7 +11266,25 @@ 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). + */ + 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`. @@ -17270,6 +17306,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -17289,7 +17328,25 @@ 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). + */ + 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 @@ -18885,6 +18942,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -18904,7 +18964,25 @@ 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). + */ + 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`. @@ -23588,6 +23666,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -23607,7 +23688,25 @@ 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). + */ + 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`. @@ -24225,6 +24324,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ httpCredentials?: { username: string; @@ -24244,7 +24346,25 @@ 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). + */ + 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`. @@ -25457,8 +25577,11 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. */ - httpCredentials?: HTTPCredentials; + httpCredentials?: HTTPCredentials|Array; /** * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 7a1ec114f0b33..bcabe8b32f84f 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -7446,6 +7446,9 @@ 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 use different credentials for different origins. The first entry that matches the request origin + * is used, and entries with no origin match any request. + * * **Usage** * * ```js @@ -7463,7 +7466,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 c87e60bd75af2..5c1505513ccf6 100644 --- a/packages/protocol/spec/browserContext.yml +++ b/packages/protocol/spec/browserContext.yml @@ -136,11 +136,8 @@ BrowserContext: group: configuration parameters: httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? + type: array? + items: HttpCredentials setNetworkInterceptionPatterns: title: Route requests diff --git a/packages/protocol/spec/electron.yml b/packages/protocol/spec/electron.yml index 877b4077469af..41a659831f60a 100644 --- a/packages/protocol/spec/electron.yml +++ b/packages/protocol/spec/electron.yml @@ -52,11 +52,8 @@ Electron: latitude: float accuracy: float? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? + type: array? + items: HttpCredentials ignoreHTTPSErrors: boolean? locale: string? offline: boolean? diff --git a/packages/protocol/spec/mixins.yml b/packages/protocol/spec/mixins.yml index 36c197654b6ca..80dcdfe8f47fb 100644 --- a/packages/protocol/spec/mixins.yml +++ b/packages/protocol/spec/mixins.yml @@ -94,6 +94,18 @@ LaunchOptions: firefoxUserPrefs: json? cdpPort: int? +HttpCredentials: + type: object + properties: + username: string + password: string + origin: string? + send: + type: enum? + literals: + - always + - unauthorized + ContextOptions: type: mixin properties: @@ -138,16 +150,8 @@ ContextOptions: items: NameValue offline: boolean? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized + type: array? + items: HttpCredentials deviceScaleFactor: float? isMobile: boolean? hasTouch: boolean? diff --git a/packages/protocol/spec/playwright.yml b/packages/protocol/spec/playwright.yml index de3634d75b1fb..7eac6215db960 100644 --- a/packages/protocol/spec/playwright.yml +++ b/packages/protocol/spec/playwright.yml @@ -64,16 +64,8 @@ Playwright: pfx: binary? maxRedirects: int? httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized + type: array? + items: HttpCredentials proxy: type: object? properties: diff --git a/packages/protocol/src/structs.d.ts b/packages/protocol/src/structs.d.ts index 30ee9c10c1149..9ee0c14461818 100644 --- a/packages/protocol/src/structs.d.ts +++ b/packages/protocol/src/structs.d.ts @@ -110,6 +110,13 @@ export type WaitInfo = { error?: string, }; +export type HttpCredentials = { + username: string, + password: string, + origin?: string, + send?: 'always' | 'unauthorized', +}; + export type SetNetworkCookie = { name: string, value: string, diff --git a/packages/protocol/src/validator.ts b/packages/protocol/src/validator.ts index d4c876cb078bf..4d09a0d987641 100644 --- a/packages/protocol/src/validator.ts +++ b/packages/protocol/src/validator.ts @@ -172,12 +172,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -458,12 +453,7 @@ scheme.BrowserNewContextParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -536,12 +526,7 @@ scheme.BrowserNewContextForReuseParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -636,12 +621,7 @@ scheme.BrowserContextInitializer = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -808,11 +788,7 @@ scheme.BrowserContextSetGeolocationParams = tObject({ }); scheme.BrowserContextSetGeolocationResult = tOptional(tObject({})); scheme.BrowserContextSetHTTPCredentialsParams = tObject({ - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), }); scheme.BrowserContextSetHTTPCredentialsResult = tOptional(tObject({})); scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({ @@ -1038,12 +1014,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), offline: tOptional(tBoolean), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), deviceScaleFactor: tOptional(tFloat), isMobile: tOptional(tBoolean), hasTouch: tOptional(tBoolean), @@ -1138,11 +1109,7 @@ scheme.ElectronLaunchParams = tObject({ latitude: tFloat, accuracy: tOptional(tFloat), })), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), ignoreHTTPSErrors: tOptional(tBoolean), locale: tOptional(tString), offline: tOptional(tBoolean), @@ -2013,6 +1980,12 @@ scheme.LocalUtilsGlobToRegexParams = tObject({ scheme.LocalUtilsGlobToRegexResult = tObject({ regex: tString, }); +scheme.HttpCredentials = tObject({ + username: tString, + password: tString, + origin: tOptional(tString), + send: tOptional(tEnum(['always', 'unauthorized'])), +}); scheme.SetNetworkCookie = tObject({ name: tString, value: tString, @@ -2687,12 +2660,7 @@ scheme.PlaywrightNewRequestParams = tObject({ pfx: tOptional(tBinary), }))), maxRedirects: tOptional(tInt), - httpCredentials: tOptional(tObject({ - username: tString, - password: tString, - origin: tOptional(tString), - send: tOptional(tEnum(['always', 'unauthorized'])), - })), + httpCredentials: tOptional(tArray(tType('HttpCredentials'))), 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..4713245ff90a3 100644 --- a/tests/library/browsercontext-credentials.spec.ts +++ b/tests/library/browsercontext-credentials.spec.ts @@ -51,6 +51,22 @@ it('should work with setHTTPCredentials', async ({ browser, server, failsOn401 } await context.close(); }); +it('should work with setHTTPCredentials and multiple credentials', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const context = await browser.newContext(); + const page = await context.newPage(); + await context.setHTTPCredentials([ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ]); + const response1 = await page.goto(server.EMPTY_PAGE); + expect(response1!.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2!.status()).toBe(401); + await context.close(); +}); + it('should work with correct credentials @smoke', async ({ browser, server }) => { server.setAuth('/empty.html', 'user', 'pass'); const context = await browser.newContext({ @@ -86,6 +102,65 @@ it('should return resource body', async ({ browser, server }) => { await context.close(); }); +it('should work with a single credential in an 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(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(200); + await context.close(); +}); + +it('should work with multiple credentials for different origins', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + 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(); + const response1 = await page.goto(server.EMPTY_PAGE); + expect(response1!.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2!.status()).toBe(401); + await context.close(); +}); + +it('should fall back to credentials without origin', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: [ + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + { username: 'user', password: 'pass' }, + ] + }); + const page = await context.newPage(); + const response1 = await page.goto(server.EMPTY_PAGE); + expect(response1!.status()).toBe(200); + // First matching entry has wrong credentials for this origin. + const response2 = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2!.status()).toBe(401); + await context.close(); +}); + +it('should use the first matching credential', async ({ browser, server, browserName }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: [ + { username: 'wrong', password: 'wrong' }, + { username: 'user', password: 'pass', origin: server.PREFIX }, + ] + }); + const page = await context.newPage(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(401); + await context.close(); +}); + it('should work with correct credentials and matching origin', async ({ browser, server }) => { server.setAuth('/empty.html', 'user', 'pass'); const context = await browser.newContext({ diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 54f219d2cd0c6..64d54e58dcd30 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -563,6 +563,21 @@ it('should return error with wrong credentials', async ({ context, server }) => expect(response2.status()).toBe(401); }); +it('should support multiple httpCredentials', async ({ contextFactory, server, browserName }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const context = await contextFactory({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + const response1 = await context.request.get(server.EMPTY_PAGE); + expect(response1.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await context.request.get(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2.status()).toBe(401); +}); + it('should support HTTPCredentials.send for newContext', async ({ contextFactory, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' }); const context = await contextFactory({ diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts index d980b253f0f2a..8a92169fab4f0 100644 --- a/tests/library/global-fetch.spec.ts +++ b/tests/library/global-fetch.spec.ts @@ -220,6 +220,49 @@ it('should support HTTPCredentials.send', async ({ playwright, server }) => { await request.dispose(); }); +it('should support multiple httpCredentials', async ({ playwright, server }) => { + server.setAuth('/empty.html', 'user1', 'pass1'); + const request = await playwright.request.newContext({ + httpCredentials: [ + { username: 'user1', password: 'pass1', origin: server.PREFIX }, + { username: 'user2', password: 'pass2', origin: server.CROSS_PROCESS_PREFIX }, + ] + }); + const response1 = await request.get(server.EMPTY_PAGE); + expect(response1.status()).toBe(200); + // Wrong credentials are picked for the other origin. + const response2 = await request.get(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response2.status()).toBe(401); + await request.dispose(); +}); + +it('should support HTTPCredentials.send with multiple httpCredentials', async ({ playwright, server }) => { + 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: 'unauthorized' }, + ] + }); + { + 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') + ]); + // This origin has send: 'unauthorized', so credentials are not sent proactively. + expect(serverRequest.headers.authorization).toBe(undefined); + 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/doclint/documentation.js b/utils/doclint/documentation.js index 1fbc1bfe9003d..3858d8f9bc44d 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -892,6 +892,9 @@ function csharpOptionOverloadSuffix(option, type) { case 'int': return 'Int'; case 'long': return 'Int64'; case 'Date': return 'Date'; + // Object keeps the original option name, e.g. for `Object|Array` unions. + case 'Object': return ''; + case 'Array': return 'List'; } throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`); } diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index ee7f2e13246f6..48849be555e6e 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -280,7 +280,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;