3030use HeadlessChromium \Exception \TargetDestroyed ;
3131use HeadlessChromium \Input \Keyboard ;
3232use HeadlessChromium \Input \Mouse ;
33+ use HeadlessChromium \PageUtils \AuthenticationScope ;
3334use HeadlessChromium \PageUtils \CookiesGetter ;
3435use HeadlessChromium \PageUtils \PageEvaluation ;
3536use HeadlessChromium \PageUtils \PageLayoutMetrics ;
@@ -84,6 +85,27 @@ class Page
8485 */
8586 protected $ authCredentials ;
8687
88+ /**
89+ * Scope the authentication credentials are restricted to.
90+ *
91+ * @var AuthenticationScope|null
92+ */
93+ protected $ authScope ;
94+
95+ /**
96+ * Challenges that were already provided the credentials.
97+ *
98+ * @var array<string, true>
99+ */
100+ protected $ attemptedAuthentications = [];
101+
102+ /**
103+ * Whether the authentication challenge listeners were registered.
104+ *
105+ * @var bool
106+ */
107+ protected $ authListenersRegistered = false ;
108+
87109 /**
88110 * Page constructor.
89111 *
@@ -170,48 +192,102 @@ public function getSession(): Session
170192 * Sets credentials to be used when the page encounters an HTTP authentication challenge,
171193 * such as basic authentication or a proxy requiring authentication.
172194 *
173- * Calling this method again replaces the credentials. If the browser rejects the
174- * credentials by challenging the same request again, the request is cancelled and an
195+ * When an origin is given, the credentials are only used to answer challenges coming from
196+ * that origin, compared by scheme, host and port, and only requests within that origin are
197+ * intercepted. Omit the origin to answer every challenge, e.g. when authenticating to a proxy.
198+ *
199+ * Calling this method again replaces the credentials and the origin. If the browser rejects
200+ * the credentials by repeating a challenge, the request is cancelled and an
175201 * AuthenticationFailed exception is thrown from the method that is reading the browser
176202 * messages at that time, usually PageNavigation::waitForNavigation.
177203 *
204+ * @throws \InvalidArgumentException
178205 * @throws CommunicationException
179206 * @throws NoResponseAvailable
180207 * @throws OperationTimedOut
181208 */
182- public function authenticate (string $ username , string $ password ): void
209+ public function authenticate (string $ username , string $ password, ? string $ origin = null ): void
183210 {
184211 $ this ->assertNotClosed ();
185212
186- $ alreadyEnabled = null !== $ this -> authCredentials ;
213+ $ scope = null !== $ origin ? new AuthenticationScope ( $ origin ) : null ;
187214
188215 $ this ->authCredentials = ['username ' => $ username , 'password ' => $ password ];
216+ $ this ->authScope = $ scope ;
217+ $ this ->attemptedAuthentications = [];
189218
190- if ($ alreadyEnabled ) {
219+ $ this ->registerAuthenticationListeners ();
220+
221+ $ this ->getSession ()->sendMessageSync (
222+ new Message ('Fetch.enable ' , [
223+ 'handleAuthRequests ' => true ,
224+ 'patterns ' => [
225+ ['urlPattern ' => null !== $ scope ? $ scope ->getUrlPattern () : '* ' ],
226+ ],
227+ ])
228+ );
229+ }
230+
231+ /**
232+ * Removes the credentials set with Page::authenticate and stops intercepting requests.
233+ *
234+ * @throws CommunicationException
235+ * @throws NoResponseAvailable
236+ * @throws OperationTimedOut
237+ */
238+ public function clearAuthentication (): void
239+ {
240+ $ this ->assertNotClosed ();
241+
242+ if (null === $ this ->authCredentials ) {
243+ return ;
244+ }
245+
246+ $ this ->authCredentials = null ;
247+ $ this ->authScope = null ;
248+ $ this ->attemptedAuthentications = [];
249+
250+ $ this ->getSession ()->sendMessageSync (new Message ('Fetch.disable ' ));
251+ }
252+
253+ /**
254+ * Registers the fetch domain listeners used to answer authentication challenges.
255+ */
256+ private function registerAuthenticationListeners (): void
257+ {
258+ if ($ this ->authListenersRegistered ) {
191259 return ;
192260 }
193261
194- // requests that were already provided the credentials; a second
195- // challenge for the same request means they were rejected
196- $ attemptedAuthentications = [];
262+ $ this ->authListenersRegistered = true ;
197263
198- $ this ->getSession ()->on ('method:Fetch.authRequired ' , function (array $ params ) use (& $ attemptedAuthentications ) : void {
264+ $ this ->getSession ()->on ('method:Fetch.authRequired ' , function (array $ params ): void {
199265 $ requestId = $ params ['requestId ' ];
266+ $ challenge = $ params ['authChallenge ' ] ?? [];
200267
201- if (\array_key_exists ($ requestId , $ attemptedAuthentications )) {
202- $ this ->getSession ()->sendMessageSync (
203- new Message ('Fetch.continueWithAuth ' , [
204- 'requestId ' => $ requestId ,
205- 'authChallengeResponse ' => [
206- 'response ' => 'CancelAuth ' ,
207- ],
208- ])
209- );
268+ $ attemptKey = \implode ("\0" , [
269+ $ requestId ,
270+ $ challenge ['source ' ] ?? '' ,
271+ $ challenge ['origin ' ] ?? '' ,
272+ $ challenge ['scheme ' ] ?? '' ,
273+ $ challenge ['realm ' ] ?? '' ,
274+ ]);
275+
276+ // a challenge that was already provided the credentials means they were rejected
277+ if (\array_key_exists ($ attemptKey , $ this ->attemptedAuthentications )) {
278+ $ this ->cancelAuthenticationChallenge ($ requestId );
210279
211280 throw new AuthenticationFailed ('Authentication failed: invalid credentials. ' );
212281 }
213282
214- $ attemptedAuthentications [$ requestId ] = true ;
283+ // cancel out of scope challenges without exposing the credentials
284+ if (null === $ this ->authCredentials || (null !== $ this ->authScope && !$ this ->authScope ->matchesOrigin ($ challenge ['origin ' ] ?? '' ))) {
285+ $ this ->cancelAuthenticationChallenge ($ requestId );
286+
287+ return ;
288+ }
289+
290+ $ this ->attemptedAuthentications [$ attemptKey ] = true ;
215291
216292 $ this ->getSession ()->sendMessageSync (
217293 new Message ('Fetch.continueWithAuth ' , [
@@ -232,12 +308,18 @@ public function authenticate(string $username, string $password): void
232308 ])
233309 );
234310 });
311+ }
235312
313+ /**
314+ * Cancels an authentication challenge without exposing the credentials.
315+ */
316+ private function cancelAuthenticationChallenge (string $ requestId ): void
317+ {
236318 $ this ->getSession ()->sendMessageSync (
237- new Message ('Fetch.enable ' , [
238- 'handleAuthRequests ' => true ,
239- 'patterns ' => [
240- [ ' urlPattern ' => '* ' ] ,
319+ new Message ('Fetch.continueWithAuth ' , [
320+ 'requestId ' => $ requestId ,
321+ 'authChallengeResponse ' => [
322+ ' response ' => 'CancelAuth ' ,
241323 ],
242324 ])
243325 );
0 commit comments