Skip to main content

better_auth_api/plugins/oauth/
token.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use base64::Engine;
6use better_auth_core::{AuthError, AuthResult};
7use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
8
9const CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
10
11/// Grant for which a token endpoint request is authenticated.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TokenGrantType {
14    /// Exchange an authorization code for tokens.
15    AuthorizationCode,
16    /// Exchange a refresh token for new tokens.
17    RefreshToken,
18}
19
20impl TokenGrantType {
21    /// Return the OAuth grant identifier.
22    pub fn as_str(self) -> &'static str {
23        match self {
24            Self::AuthorizationCode => "authorization_code",
25            Self::RefreshToken => "refresh_token",
26        }
27    }
28}
29
30/// Values used to create an RFC 7523 client assertion for one request.
31#[derive(Debug, Clone, Copy)]
32pub struct ClientAssertionContext<'a> {
33    /// OAuth client identifier.
34    pub client_id: &'a str,
35    /// Final token endpoint URL, used as the assertion audience.
36    pub token_endpoint: &'a str,
37    /// Grant carried by this request.
38    pub grant_type: TokenGrantType,
39}
40
41/// Creates a fresh, signed client assertion for each token request.
42#[async_trait]
43pub trait ClientAssertion: Send + Sync {
44    /// Return an RFC 7523 JWT assertion signed by the client's private key.
45    async fn get_client_assertion(&self, context: ClientAssertionContext<'_>)
46    -> AuthResult<String>;
47}
48
49/// Mutable request passed to a custom token endpoint authentication strategy.
50pub struct TokenEndpointRequestContext<'a> {
51    /// Form parameters, including any repeated resource parameters.
52    pub body: &'a mut Vec<(String, String)>,
53    /// HTTP headers sent to the token endpoint.
54    pub headers: &'a mut HeaderMap,
55    /// OAuth client identifier.
56    pub client_id: &'a str,
57    /// Optional configured client secret.
58    pub client_secret: Option<&'a str>,
59    /// Final token endpoint URL.
60    pub token_endpoint: &'a str,
61    /// Grant carried by this request.
62    pub grant_type: TokenGrantType,
63}
64
65/// Applies custom authentication after standard grant parameters are set.
66#[async_trait]
67pub trait TokenRequestHook: Send + Sync {
68    /// Update the request body and headers, or reject the request.
69    async fn customize_request(&self, context: TokenEndpointRequestContext<'_>) -> AuthResult<()>;
70}
71
72/// Authentication method used for authorization-code exchange and token refresh.
73#[derive(Clone)]
74pub enum TokenEndpointAuth {
75    /// Send the client identifier and secret in the request body.
76    ClientSecretPost,
77    /// Send form-encoded credentials in the HTTP Basic authorization header.
78    ClientSecretBasic,
79    /// Authenticate a public client with its identifier and no secret.
80    None,
81    /// Obtain a signed RFC 7523 assertion for each request.
82    PrivateKeyJwt(Arc<dyn ClientAssertion>),
83    /// Apply a provider-specific authentication strategy.
84    Custom(Arc<dyn TokenRequestHook>),
85}
86
87/// Legacy OAuth client-secret authentication selection.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TokenEndpointSecretAuthentication {
90    /// Send form-encoded credentials in the HTTP Basic authorization header.
91    Basic,
92    /// Send credentials in the request body.
93    Post,
94}
95
96pub(super) struct TokenAuthentication<'a> {
97    pub client_id: &'a str,
98    pub client_secret: Option<&'a str>,
99    pub token_endpoint: &'a str,
100    pub grant_type: TokenGrantType,
101    pub token_endpoint_auth: Option<&'a TokenEndpointAuth>,
102    pub authentication: Option<TokenEndpointSecretAuthentication>,
103}
104
105pub(super) struct AuthorizationCodeRequest<'a> {
106    pub code: &'a str,
107    pub redirect_uri: &'a str,
108    pub code_verifier: Option<&'a str>,
109    pub client_key: Option<&'a str>,
110    pub device_id: Option<&'a str>,
111    pub headers: &'a HeaderMap,
112    pub additional_params: &'a HashMap<String, String>,
113    pub resources: &'a [String],
114}
115
116pub(super) struct TokenRequest {
117    pub body: Vec<(String, String)>,
118    pub headers: HeaderMap,
119}
120
121impl TokenRequest {
122    fn new(grant_type: TokenGrantType) -> Self {
123        let mut headers = HeaderMap::new();
124        let _ = headers.insert(
125            CONTENT_TYPE,
126            HeaderValue::from_static("application/x-www-form-urlencoded"),
127        );
128        let _ = headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
129        Self {
130            body: vec![("grant_type".to_string(), grant_type.as_str().to_string())],
131            headers,
132        }
133    }
134
135    pub(super) fn authorization_code(input: AuthorizationCodeRequest<'_>) -> Self {
136        let mut request = Self::new(TokenGrantType::AuthorizationCode);
137        request.headers.extend(input.headers.clone());
138        request.set("code", input.code);
139        for (key, value) in [
140            ("code_verifier", input.code_verifier),
141            ("client_key", input.client_key),
142            ("device_id", input.device_id),
143        ] {
144            if let Some(value) = value.filter(|value| !value.is_empty()) {
145                request.set(key, value);
146            }
147        }
148        request.set("redirect_uri", input.redirect_uri);
149        request.add_resources(input.resources);
150        for (key, value) in input.additional_params {
151            if !request.has(key) {
152                request.set(key, value);
153            }
154        }
155        request
156    }
157
158    pub(super) fn refresh_token(
159        refresh_token: &str,
160        extra_params: &HashMap<String, String>,
161        resources: &[String],
162    ) -> Self {
163        let mut request = Self::new(TokenGrantType::RefreshToken);
164        request.set("refresh_token", refresh_token);
165        request.add_resources(resources);
166        for (key, value) in extra_params {
167            if !matches!(
168                key.as_str(),
169                "grant_type" | "refresh_token" | "__proto__" | "constructor" | "prototype"
170            ) {
171                request.set(key, value);
172            }
173        }
174        request
175    }
176
177    pub(super) async fn authenticate(&mut self, input: TokenAuthentication<'_>) -> AuthResult<()> {
178        self.assert_complete_assertion()?;
179        if self.has("client_assertion") {
180            if input.token_endpoint_auth.is_some() {
181                return Err(AuthError::internal(
182                    "client_assertion body parameters cannot be combined with tokenEndpointAuth",
183                ));
184            }
185            self.assert_no_client_secret("private_key_jwt", input.client_secret)?;
186            if !input.client_id.is_empty() {
187                self.set("client_id", input.client_id);
188            }
189            return Ok(());
190        }
191
192        let default_auth = match input.authentication {
193            Some(TokenEndpointSecretAuthentication::Basic) => TokenEndpointAuth::ClientSecretBasic,
194            _ if input.client_secret.is_some_and(|secret| !secret.is_empty()) => {
195                TokenEndpointAuth::ClientSecretPost
196            }
197            _ => TokenEndpointAuth::None,
198        };
199        match input.token_endpoint_auth.unwrap_or(&default_auth) {
200            TokenEndpointAuth::Custom(hook) => {
201                hook.customize_request(TokenEndpointRequestContext {
202                    body: &mut self.body,
203                    headers: &mut self.headers,
204                    client_id: input.client_id,
205                    client_secret: input.client_secret,
206                    token_endpoint: input.token_endpoint,
207                    grant_type: input.grant_type,
208                })
209                .await?;
210                self.assert_complete_assertion()?;
211            }
212            TokenEndpointAuth::PrivateKeyJwt(assertion) => {
213                self.assert_no_client_secret("private_key_jwt", input.client_secret)?;
214                require_client_id("private_key_jwt", input.client_id)?;
215                if input.token_endpoint.is_empty() {
216                    return Err(AuthError::internal(
217                        "private_key_jwt token endpoint authentication requires tokenEndpoint",
218                    ));
219                }
220                let assertion = assertion
221                    .get_client_assertion(ClientAssertionContext {
222                        client_id: input.client_id,
223                        token_endpoint: input.token_endpoint,
224                        grant_type: input.grant_type,
225                    })
226                    .await?;
227                self.set("client_id", input.client_id);
228                self.set("client_assertion", &assertion);
229                self.set("client_assertion_type", CLIENT_ASSERTION_TYPE);
230            }
231            TokenEndpointAuth::None => {
232                self.assert_no_client_secret("none", input.client_secret)?;
233                require_client_id("none", input.client_id)?;
234                self.set("client_id", input.client_id);
235            }
236            TokenEndpointAuth::ClientSecretBasic => {
237                if self.has("client_secret") {
238                    return Err(AuthError::internal(
239                        "client_secret_basic token endpoint authentication cannot be combined with client_secret body parameters",
240                    ));
241                }
242                let secret = require_client_secret("client_secret_basic", input.client_secret)?;
243                require_client_id("client_secret_basic", input.client_id)?;
244                let credentials =
245                    format!("{}:{}", form_encode(input.client_id), form_encode(secret));
246                let authorization = format!(
247                    "Basic {}",
248                    base64::engine::general_purpose::STANDARD.encode(credentials)
249                );
250                let _ = self.headers.insert(
251                    AUTHORIZATION,
252                    HeaderValue::from_str(&authorization).map_err(|error| {
253                        AuthError::internal(format!("Invalid client authorization header: {error}"))
254                    })?,
255                );
256            }
257            TokenEndpointAuth::ClientSecretPost => {
258                let secret = require_client_secret("client_secret_post", input.client_secret)?;
259                require_client_id("client_secret_post", input.client_id)?;
260                self.set("client_id", input.client_id);
261                self.set("client_secret", secret);
262            }
263        }
264        Ok(())
265    }
266
267    pub(super) async fn send(self, token_endpoint: &str) -> AuthResult<serde_json::Value> {
268        send_request(
269            client()?
270                .post(token_endpoint)
271                .headers(self.headers)
272                .form(&self.body),
273            token_endpoint,
274        )
275        .await
276    }
277
278    pub(super) async fn send_query(
279        token_endpoint: &str,
280        params: &[(&str, &str)],
281    ) -> AuthResult<serde_json::Value> {
282        send_request(client()?.get(token_endpoint).query(params), token_endpoint).await
283    }
284
285    fn has(&self, name: &str) -> bool {
286        self.body.iter().any(|(key, _)| key == name)
287    }
288
289    fn set(&mut self, name: &str, value: &str) {
290        let mut updated = false;
291        self.body.retain_mut(|(key, current)| {
292            if key != name {
293                return true;
294            }
295            if updated {
296                return false;
297            }
298            *current = value.to_string();
299            updated = true;
300            true
301        });
302        if !updated {
303            self.body.push((name.to_string(), value.to_string()));
304        }
305    }
306
307    fn add_resources(&mut self, resources: &[String]) {
308        self.body.extend(
309            resources
310                .iter()
311                .map(|value| ("resource".to_string(), value.clone())),
312        );
313    }
314
315    fn assert_complete_assertion(&self) -> AuthResult<()> {
316        if self.has("client_assertion") != self.has("client_assertion_type") {
317            return Err(AuthError::internal(
318                "client_assertion and client_assertion_type must both be provided",
319            ));
320        }
321        Ok(())
322    }
323
324    fn assert_no_client_secret(&self, method: &str, client_secret: Option<&str>) -> AuthResult<()> {
325        if client_secret.is_some_and(|secret| !secret.is_empty()) || self.has("client_secret") {
326            return Err(AuthError::internal(format!(
327                "{method} token endpoint authentication cannot be combined with clientSecret"
328            )));
329        }
330        Ok(())
331    }
332}
333
334fn require_client_id(method: &str, client_id: &str) -> AuthResult<()> {
335    if client_id.is_empty() {
336        return Err(AuthError::internal(format!(
337            "{method} token endpoint authentication requires clientId"
338        )));
339    }
340    Ok(())
341}
342
343fn require_client_secret<'a>(method: &str, client_secret: Option<&'a str>) -> AuthResult<&'a str> {
344    client_secret
345        .filter(|secret| !secret.is_empty())
346        .ok_or_else(|| {
347            AuthError::internal(format!(
348                "{method} token endpoint authentication requires clientSecret"
349            ))
350        })
351}
352
353fn form_encode(value: &str) -> String {
354    url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
355}
356
357fn client() -> AuthResult<reqwest::Client> {
358    reqwest::Client::builder()
359        .redirect(reqwest::redirect::Policy::none())
360        .build()
361        .map_err(|error| AuthError::internal(format!("OAuth HTTP client failed: {error}")))
362}
363
364async fn send_request(
365    request: reqwest::RequestBuilder,
366    token_endpoint: &str,
367) -> AuthResult<serde_json::Value> {
368    // GET grants carry credentials in the query. Keep request URLs out of errors.
369    let response = request.send().await.map_err(|error| {
370        AuthError::internal(format!("Token request failed: {}", error.without_url()))
371    })?;
372    if matches!(response.status().as_u16(), 301 | 302 | 303 | 307 | 308) {
373        return Err(AuthError::internal(format!(
374            "The OAuth endpoint \"{token_endpoint}\" returned an HTTP redirect. Server-side OAuth fetches refuse redirects to prevent SSRF; configure the final endpoint URL."
375        )));
376    }
377    let response = response.error_for_status().map_err(|error| {
378        AuthError::internal(format!("Token request failed: {}", error.without_url()))
379    })?;
380    response.json().await.map_err(|error| {
381        AuthError::internal(format!(
382            "Failed to parse token response: {}",
383            error.without_url()
384        ))
385    })
386}
387
388#[cfg(test)]
389#[path = "token_tests.rs"]
390mod tests;