@@ -42,6 +42,12 @@ pub struct OidcAuth {
4242 #[ serde( default = "default_timeout" ) ]
4343 timeout : f64 ,
4444
45+ /// Allow token extraction from the `access_token` query parameter
46+ /// (RFC 6750 §2.3). Disabled by default — tokens in URLs risk leaking
47+ /// via logs, referer headers, and browser history.
48+ #[ serde( default ) ]
49+ allow_query_token : bool ,
50+
4551 /// Cached OIDC discovery document.
4652 #[ serde( skip) ]
4753 discovery : Option < DiscoveryDoc > ,
@@ -63,6 +69,28 @@ fn default_timeout() -> f64 {
6369 5.0
6470}
6571
72+ /// Minimal percent-decoding for query parameter values (RFC 3986).
73+ fn percent_decode ( input : & str ) -> String {
74+ let mut out = Vec :: with_capacity ( input. len ( ) ) ;
75+ let bytes = input. as_bytes ( ) ;
76+ let mut i = 0 ;
77+ while i < bytes. len ( ) {
78+ if bytes[ i] == b'%' && i + 2 < bytes. len ( ) {
79+ if let Ok ( byte) = u8:: from_str_radix (
80+ & input[ i + 1 ..i + 3 ] ,
81+ 16 ,
82+ ) {
83+ out. push ( byte) ;
84+ i += 3 ;
85+ continue ;
86+ }
87+ }
88+ out. push ( bytes[ i] ) ;
89+ i += 1 ;
90+ }
91+ String :: from_utf8_lossy ( & out) . into_owned ( )
92+ }
93+
6694// --- Internal types ---
6795
6896/// Cached OIDC discovery document.
@@ -354,19 +382,36 @@ impl OidcAuth {
354382 Ok ( parsed. claims )
355383 }
356384
357- /// Extract Bearer token from Authorization header.
385+ /// Extract Bearer token from Authorization header, falling back to the
386+ /// `access_token` query parameter when `allow_query_token` is enabled
387+ /// (RFC 6750 §2.3).
358388 fn extract_token ( & self , req : & Request ) -> Result < String , OidcError > {
359- let auth_header = req
389+ if let Some ( auth_header) = req
360390 . headers
361391 . get ( "authorization" )
362392 . or_else ( || req. headers . get ( "Authorization" ) )
363- . ok_or ( OidcError :: MissingToken ) ?;
393+ {
394+ if !auth_header. starts_with ( "Bearer " ) && !auth_header. starts_with ( "bearer " ) {
395+ return Err ( OidcError :: InvalidAuthHeader ) ;
396+ }
397+ return Ok ( auth_header[ 7 ..] . trim ( ) . to_string ( ) ) ;
398+ }
364399
365- if !auth_header. starts_with ( "Bearer " ) && !auth_header. starts_with ( "bearer " ) {
366- return Err ( OidcError :: InvalidAuthHeader ) ;
400+ // RFC 6750 §2.3 — query parameter fallback
401+ if self . allow_query_token {
402+ if let Some ( query) = & req. query {
403+ for pair in query. split ( '&' ) {
404+ if let Some ( value) = pair. strip_prefix ( "access_token=" ) {
405+ let token = percent_decode ( value) ;
406+ if !token. is_empty ( ) {
407+ return Ok ( token) ;
408+ }
409+ }
410+ }
411+ }
367412 }
368413
369- Ok ( auth_header [ 7 .. ] . trim ( ) . to_string ( ) )
414+ Err ( OidcError :: MissingToken )
370415 }
371416
372417 /// Parse a JWT token into its components.
@@ -774,6 +819,7 @@ mod tests {
774819 jwks_refresh_seconds : 300 ,
775820 issuer_override : None ,
776821 timeout : 5.0 ,
822+ allow_query_token : false ,
777823 discovery : None ,
778824 jwks_cache : None ,
779825 }
@@ -892,6 +938,95 @@ mod tests {
892938 assert_eq ! ( token, "cap.token.here" ) ;
893939 }
894940
941+ // --- Query parameter token tests (RFC 6750 §2.3) ---
942+
943+ fn create_query_request ( query : Option < & str > ) -> Request {
944+ Request {
945+ method : "GET" . to_string ( ) ,
946+ path : "/test" . to_string ( ) ,
947+ headers : BTreeMap :: new ( ) ,
948+ body : None ,
949+ query : query. map ( |q| q. to_string ( ) ) ,
950+ path_params : BTreeMap :: new ( ) ,
951+ client_ip : "127.0.0.1" . to_string ( ) ,
952+ }
953+ }
954+
955+ #[ test]
956+ fn extract_token_query_param_when_enabled ( ) {
957+ let mut config = create_test_config ( ) ;
958+ config. allow_query_token = true ;
959+ let req = create_query_request ( Some ( "access_token=my.jwt.token&foo=bar" ) ) ;
960+ let token = config. extract_token ( & req) . unwrap ( ) ;
961+ assert_eq ! ( token, "my.jwt.token" ) ;
962+ }
963+
964+ #[ test]
965+ fn extract_token_query_param_disabled_by_default ( ) {
966+ let config = create_test_config ( ) ;
967+ let req = create_query_request ( Some ( "access_token=my.jwt.token" ) ) ;
968+ let result = config. extract_token ( & req) ;
969+ assert ! ( matches!( result, Err ( OidcError :: MissingToken ) ) ) ;
970+ }
971+
972+ #[ test]
973+ fn extract_token_header_takes_precedence_over_query ( ) {
974+ let mut config = create_test_config ( ) ;
975+ config. allow_query_token = true ;
976+ let mut headers = BTreeMap :: new ( ) ;
977+ headers. insert (
978+ "authorization" . to_string ( ) ,
979+ "Bearer header.token.here" . to_string ( ) ,
980+ ) ;
981+ let req = Request {
982+ method : "GET" . to_string ( ) ,
983+ path : "/test" . to_string ( ) ,
984+ headers,
985+ body : None ,
986+ query : Some ( "access_token=query.token.here" . to_string ( ) ) ,
987+ path_params : BTreeMap :: new ( ) ,
988+ client_ip : "127.0.0.1" . to_string ( ) ,
989+ } ;
990+ let token = config. extract_token ( & req) . unwrap ( ) ;
991+ assert_eq ! ( token, "header.token.here" ) ;
992+ }
993+
994+ #[ test]
995+ fn extract_token_query_param_percent_encoded ( ) {
996+ let mut config = create_test_config ( ) ;
997+ config. allow_query_token = true ;
998+ let req = create_query_request ( Some ( "access_token=eyJ%2Btoken%3D" ) ) ;
999+ let token = config. extract_token ( & req) . unwrap ( ) ;
1000+ assert_eq ! ( token, "eyJ+token=" ) ;
1001+ }
1002+
1003+ #[ test]
1004+ fn extract_token_query_param_empty_value ( ) {
1005+ let mut config = create_test_config ( ) ;
1006+ config. allow_query_token = true ;
1007+ let req = create_query_request ( Some ( "access_token=" ) ) ;
1008+ let result = config. extract_token ( & req) ;
1009+ assert ! ( matches!( result, Err ( OidcError :: MissingToken ) ) ) ;
1010+ }
1011+
1012+ #[ test]
1013+ fn extract_token_query_param_missing ( ) {
1014+ let mut config = create_test_config ( ) ;
1015+ config. allow_query_token = true ;
1016+ let req = create_query_request ( Some ( "foo=bar&baz=qux" ) ) ;
1017+ let result = config. extract_token ( & req) ;
1018+ assert ! ( matches!( result, Err ( OidcError :: MissingToken ) ) ) ;
1019+ }
1020+
1021+ #[ test]
1022+ fn extract_token_query_param_no_query_string ( ) {
1023+ let mut config = create_test_config ( ) ;
1024+ config. allow_query_token = true ;
1025+ let req = create_query_request ( None ) ;
1026+ let result = config. extract_token ( & req) ;
1027+ assert ! ( matches!( result, Err ( OidcError :: MissingToken ) ) ) ;
1028+ }
1029+
8951030 // --- JWT parsing tests ---
8961031
8971032 #[ test]
0 commit comments