@@ -150,6 +150,88 @@ def apply_authorization(request)
150150 request . headers [ "Authorization" ] = token . to_header
151151 end
152152
153+ # Handle authentication challenge from server (401 response)
154+ # Attempts to refresh token or raises error if interactive auth required
155+ # @param www_authenticate [String, nil] WWW-Authenticate header value
156+ # @param resource_metadata_url [String, nil] Resource metadata URL from response
157+ # @param requested_scope [String, nil] Scope from WWW-Authenticate challenge
158+ # @return [Boolean] true if authentication was refreshed successfully
159+ # @raise [Errors::AuthenticationRequiredError] if interactive auth is required
160+ def handle_authentication_challenge ( www_authenticate : nil , resource_metadata_url : nil , requested_scope : nil )
161+ logger . debug ( "Handling authentication challenge" )
162+ logger . debug ( " WWW-Authenticate: #{ www_authenticate } " ) if www_authenticate
163+ logger . debug ( " Resource metadata URL: #{ resource_metadata_url } " ) if resource_metadata_url
164+ logger . debug ( " Requested scope: #{ requested_scope } " ) if requested_scope
165+
166+ # Parse WWW-Authenticate header if provided
167+ final_requested_scope = requested_scope
168+ if www_authenticate
169+ challenge_info = parse_www_authenticate ( www_authenticate )
170+ final_requested_scope ||= challenge_info [ :scope ]
171+ # NOTE: resource_metadata_url from challenge_info could be used for future discovery
172+ end
173+
174+ # Update scope if server requested different scope
175+ if final_requested_scope && final_requested_scope != scope
176+ logger . debug ( "Updating scope from '#{ scope } ' to '#{ final_requested_scope } '" )
177+ self . scope = final_requested_scope
178+ end
179+
180+ # Try to refresh existing token
181+ token = storage . get_token ( server_url )
182+ if token &.refresh_token
183+ logger . debug ( "Attempting token refresh with existing refresh token" )
184+ refreshed_token = refresh_token ( token )
185+ return true if refreshed_token
186+ end
187+
188+ # If we have client credentials, try that flow
189+ if grant_type == :client_credentials
190+ logger . debug ( "Attempting client credentials flow" )
191+ begin
192+ new_token = client_credentials_flow ( scope : requested_scope )
193+ return true if new_token
194+ rescue StandardError => e
195+ logger . warn ( "Client credentials flow failed: #{ e . message } " )
196+ end
197+ end
198+
199+ # Cannot automatically authenticate - interactive auth required
200+ logger . warn ( "Cannot automatically authenticate - interactive authorization required" )
201+ raise Errors ::AuthenticationRequiredError . new (
202+ message : "OAuth authentication required. Token refresh failed and interactive authorization is needed."
203+ )
204+ end
205+
206+ # Parse WWW-Authenticate header to extract challenge parameters
207+ # @param header [String] WWW-Authenticate header value
208+ # @return [Hash] parsed challenge information
209+ def parse_www_authenticate ( header )
210+ result = { }
211+
212+ # Example: Bearer realm="example", scope="mcp:read mcp:write", resource_metadata_url="https://..."
213+ if header =~ /Bearer\s +(.+)/i
214+ params = ::Regexp . last_match ( 1 )
215+
216+ # Extract scope
217+ if params =~ /scope="([^"]+)"/
218+ result [ :scope ] = ::Regexp . last_match ( 1 )
219+ end
220+
221+ # Extract resource metadata URL
222+ if params =~ /resource_metadata_url="([^"]+)"/
223+ result [ :resource_metadata_url ] = ::Regexp . last_match ( 1 )
224+ end
225+
226+ # Extract realm
227+ if params =~ /realm="([^"]+)"/
228+ result [ :realm ] = ::Regexp . last_match ( 1 )
229+ end
230+ end
231+
232+ result
233+ end
234+
153235 private
154236
155237 # Create HTTP client for OAuth requests
0 commit comments