@@ -269,6 +269,129 @@ def test_token_generation_form_encoded(self):
269269 self .assertIn ("individual:read" , data ["scope" ])
270270 self .assertIn ("group:search" , data ["scope" ])
271271
272+ def test_basic_auth_only_no_body (self ):
273+ """Basic Auth header with no body or Content-Type returns access token"""
274+ credentials = base64 .b64encode (f"{ self .client .client_id } :{ self .client .client_secret } " .encode ()).decode ("utf-8" )
275+
276+ response = self .url_open (
277+ self .url ,
278+ data = "{}" ,
279+ headers = {"Authorization" : f"Basic { credentials } " },
280+ )
281+
282+ self .assertEqual (response .status_code , 200 )
283+
284+ data = json .loads (response .content )
285+ self .assertIn ("access_token" , data )
286+ self .assertEqual (data ["token_type" ], "Bearer" )
287+
288+ def test_basic_auth_header_with_form_body_override (self ):
289+ """Form body credentials take precedence over Basic Auth header"""
290+ # Basic Auth header has WRONG secret
291+ wrong_credentials = base64 .b64encode (f"{ self .client .client_id } :wrong-secret" .encode ()).decode ("utf-8" )
292+
293+ # Form body has CORRECT credentials
294+ body = urlencode (
295+ {
296+ "grant_type" : "client_credentials" ,
297+ "client_id" : self .client .client_id ,
298+ "client_secret" : self .client .client_secret ,
299+ }
300+ )
301+
302+ response = self .url_open (
303+ self .url ,
304+ data = body ,
305+ headers = {
306+ "Content-Type" : "application/x-www-form-urlencoded" ,
307+ "Authorization" : f"Basic { wrong_credentials } " ,
308+ },
309+ )
310+
311+ # Should succeed because form body credentials take precedence
312+ self .assertEqual (response .status_code , 200 )
313+
314+ def test_basic_auth_supplements_form_body (self ):
315+ """Basic Auth fills in missing form body credentials"""
316+ credentials = base64 .b64encode (f"{ self .client .client_id } :{ self .client .client_secret } " .encode ()).decode ("utf-8" )
317+
318+ # Form body has grant_type only, no client_id/client_secret
319+ body = urlencode ({"grant_type" : "client_credentials" })
320+
321+ response = self .url_open (
322+ self .url ,
323+ data = body ,
324+ headers = {
325+ "Content-Type" : "application/x-www-form-urlencoded" ,
326+ "Authorization" : f"Basic { credentials } " ,
327+ },
328+ )
329+
330+ self .assertEqual (response .status_code , 200 )
331+
332+ data = json .loads (response .content )
333+ self .assertIn ("access_token" , data )
334+
335+ def test_malformed_base64_auth_header (self ):
336+ """Malformed base64 in Authorization header is ignored gracefully"""
337+ body = urlencode (
338+ {
339+ "grant_type" : "client_credentials" ,
340+ "client_id" : self .client .client_id ,
341+ "client_secret" : self .client .client_secret ,
342+ }
343+ )
344+
345+ response = self .url_open (
346+ self .url ,
347+ data = body ,
348+ headers = {
349+ "Content-Type" : "application/x-www-form-urlencoded" ,
350+ "Authorization" : "Basic !!!not-valid-base64!!!" ,
351+ },
352+ )
353+
354+ # Should still succeed via form body credentials
355+ self .assertEqual (response .status_code , 200 )
356+
357+ def test_basic_auth_no_colon_in_decoded (self ):
358+ """Basic Auth with no colon separator is ignored"""
359+ # Encode a value without colon
360+ no_colon = base64 .b64encode (b"no-colon-here" ).decode ("utf-8" )
361+
362+ body = urlencode (
363+ {
364+ "grant_type" : "client_credentials" ,
365+ "client_id" : self .client .client_id ,
366+ "client_secret" : self .client .client_secret ,
367+ }
368+ )
369+
370+ response = self .url_open (
371+ self .url ,
372+ data = body ,
373+ headers = {
374+ "Content-Type" : "application/x-www-form-urlencoded" ,
375+ "Authorization" : f"Basic { no_colon } " ,
376+ },
377+ )
378+
379+ # Should still succeed via form body credentials
380+ self .assertEqual (response .status_code , 200 )
381+
382+ def test_no_credentials_returns_400 (self ):
383+ """No credentials at all returns 400"""
384+ response = self .url_open (
385+ self .url ,
386+ data = "not-json-not-form" ,
387+ headers = {"Content-Type" : "text/plain" },
388+ )
389+
390+ self .assertEqual (response .status_code , 400 )
391+ data = json .loads (response .content )
392+ self .assertIn ("detail" , data )
393+ self .assertIn ("Unable to parse" , data ["detail" ])
394+
272395 def test_token_no_scopes (self ):
273396 """Client with no scopes still gets token but empty scope string"""
274397 # Create client without scopes
0 commit comments