better_auth_api/plugins/oauth/
token.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TokenGrantType {
14 AuthorizationCode,
16 RefreshToken,
18}
19
20impl TokenGrantType {
21 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#[derive(Debug, Clone, Copy)]
32pub struct ClientAssertionContext<'a> {
33 pub client_id: &'a str,
35 pub token_endpoint: &'a str,
37 pub grant_type: TokenGrantType,
39}
40
41#[async_trait]
43pub trait ClientAssertion: Send + Sync {
44 async fn get_client_assertion(&self, context: ClientAssertionContext<'_>)
46 -> AuthResult<String>;
47}
48
49pub struct TokenEndpointRequestContext<'a> {
51 pub body: &'a mut Vec<(String, String)>,
53 pub headers: &'a mut HeaderMap,
55 pub client_id: &'a str,
57 pub client_secret: Option<&'a str>,
59 pub token_endpoint: &'a str,
61 pub grant_type: TokenGrantType,
63}
64
65#[async_trait]
67pub trait TokenRequestHook: Send + Sync {
68 async fn customize_request(&self, context: TokenEndpointRequestContext<'_>) -> AuthResult<()>;
70}
71
72#[derive(Clone)]
74pub enum TokenEndpointAuth {
75 ClientSecretPost,
77 ClientSecretBasic,
79 None,
81 PrivateKeyJwt(Arc<dyn ClientAssertion>),
83 Custom(Arc<dyn TokenRequestHook>),
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TokenEndpointSecretAuthentication {
90 Basic,
92 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 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;