|
110 | 110 | :error-message error})) |
111 | 111 | (response/content-type "text/html")))))) |
112 | 112 |
|
| 113 | +(defn ^:private probe-auth |
| 114 | + "Probe the MCP server for a 401 auth challenge. |
| 115 | + Tries HEAD first; falls back to POST if HEAD returns non-401 |
| 116 | + (some servers like Glean don't support HEAD)." |
| 117 | + [^String url] |
| 118 | + (let [head-response (http/head url {:timeout 10000 |
| 119 | + :throw-exceptions? false})] |
| 120 | + (if (= 401 (:status head-response)) |
| 121 | + head-response |
| 122 | + (let [post-response (http/post url {:timeout 10000 |
| 123 | + :throw-exceptions? false |
| 124 | + :headers {"Content-Type" "application/json" |
| 125 | + "Accept" "application/json, text/event-stream"} |
| 126 | + :body "{}"})] |
| 127 | + (when (= 401 (:status post-response)) |
| 128 | + post-response))))) |
| 129 | + |
113 | 130 | (defn oauth-info [^String url] |
114 | 131 | (let [base-url (url->base-url url) |
115 | | - {:keys [status headers]} (http/head |
116 | | - url |
117 | | - {:timeout 10000 |
118 | | - :throw-exceptions? false})] |
119 | | - (when (= 401 status) |
| 132 | + auth-response (probe-auth url)] |
| 133 | + (when-let [headers (:headers auth-response)] |
120 | 134 | (let [callback-port (get-free-port) |
121 | 135 | redirect-uri (format "http://localhost:%s/auth/callback" callback-port) |
122 | 136 | www-authenticate (some-> (get headers "www-authenticate") parse-www-authenticate) |
123 | | - auth-resource-meta (:body (http/get |
124 | | - (or (:resource_metadata www-authenticate) |
125 | | - (str base-url "/.well-known/oauth-authorization-server")) |
126 | | - {:timeout 10000 |
127 | | - :throw-exceptions? false |
128 | | - :as :json}) |
129 | | - :body) |
130 | | - auth-server (first (:authorization_servers auth-resource-meta)) |
131 | | - base-auth-endpoint (or (:authorization_endpoint auth-resource-meta) |
| 137 | + ;; Step 1: Fetch resource metadata (PRM) or auth server metadata directly |
| 138 | + first-meta (some-> (http/get |
| 139 | + (or (:resource_metadata www-authenticate) |
| 140 | + (str base-url "/.well-known/oauth-authorization-server")) |
| 141 | + {:timeout 10000 |
| 142 | + :throw-exceptions? false |
| 143 | + :as :json}) |
| 144 | + :body) |
| 145 | + auth-server (first (:authorization_servers first-meta)) |
| 146 | + ;; Step 2: If PRM returned authorization_servers but no token_endpoint, |
| 147 | + ;; fetch the actual Authorization Server Metadata (RFC 8414) |
| 148 | + auth-server-meta (when (and auth-server (not (:token_endpoint first-meta))) |
| 149 | + (let [uri (java.net.URI. ^String auth-server) |
| 150 | + well-known-url (str (.getScheme uri) "://" (.getAuthority uri) |
| 151 | + "/.well-known/oauth-authorization-server" |
| 152 | + (.getPath uri))] |
| 153 | + (some-> (http/get well-known-url |
| 154 | + {:timeout 10000 |
| 155 | + :throw-exceptions? false |
| 156 | + :as :json}) |
| 157 | + :body))) |
| 158 | + ;; Merge: auth server metadata takes precedence, PRM as fallback |
| 159 | + meta (merge first-meta auth-server-meta) |
| 160 | + base-auth-endpoint (or (:authorization_endpoint meta) |
132 | 161 | (str auth-server "/authorize")) |
133 | | - new-client-id (when-let [reg-endpoint (:registration_endpoint auth-resource-meta)] |
| 162 | + new-client-id (when-let [reg-endpoint (:registration_endpoint meta)] |
134 | 163 | (let [res |
135 | 164 | (http/post |
136 | 165 | reg-endpoint |
|
153 | 182 | :client_id client-id |
154 | 183 | :code_challenge_method "S256" |
155 | 184 | :code_challenge challenge |
156 | | - :scopes (:scopes_supported auth-resource-meta) |
| 185 | + :scopes (:scopes_supported meta) |
157 | 186 | :state verifier |
158 | 187 | :redirect_uri redirect-uri})] |
159 | 188 | {:callback-port callback-port |
160 | | - :token-endpoint (or (:token_endpoint auth-resource-meta) |
| 189 | + :token-endpoint (or (:token_endpoint meta) |
161 | 190 | (str auth-server "/access_token")) |
162 | 191 | :verifier verifier |
163 | 192 | :client-id client-id |
|
0 commit comments