better_auth_api/plugins/oauth/
generic.rs1use std::{collections::HashMap, sync::Arc};
2
3use async_trait::async_trait;
4use better_auth_core::{AuthRequest, AuthResult};
5use indexmap::IndexMap;
6use reqwest::header::HeaderMap;
7use serde_json::Value;
8
9use super::token::{TokenEndpointAuth, TokenEndpointSecretAuthentication};
10use super::{OAuthTokenSet, OAuthUserInfoRequest};
11
12pub struct OAuthCodeExchange<'a> {
14 pub code: &'a str,
16 pub redirect_uri: &'a str,
18 pub code_verifier: Option<&'a str>,
20 pub device_id: Option<&'a str>,
22}
23
24#[async_trait]
26pub trait OAuthTokenHandler: Send + Sync {
27 async fn get_token(&self, request: OAuthCodeExchange<'_>) -> AuthResult<OAuthTokenSet>;
29}
30
31pub struct VerifiedOAuthClaims<'a> {
33 claims: &'a Value,
34}
35
36impl<'a> VerifiedOAuthClaims<'a> {
37 pub(super) fn new(claims: &'a Value) -> Self {
38 Self { claims }
39 }
40
41 pub fn as_value(&self) -> &'a Value {
43 self.claims
44 }
45}
46
47pub struct GenericOAuthProfileContext<'a> {
49 client_id: &'a str,
50 expected_nonce: Option<&'a str>,
51 user_info_url: Option<&'a str>,
52 verified_claims: Option<VerifiedOAuthClaims<'a>>,
53}
54
55impl<'a> GenericOAuthProfileContext<'a> {
56 pub(super) fn new(
57 config: &'a GenericOAuthConfig,
58 expected_nonce: Option<&'a str>,
59 verified_claims: Option<&'a Value>,
60 ) -> Self {
61 Self {
62 client_id: &config.client_id,
63 expected_nonce,
64 user_info_url: config.user_info_url.as_deref(),
65 verified_claims: verified_claims.map(VerifiedOAuthClaims::new),
66 }
67 }
68
69 pub fn client_id(&self) -> &'a str {
71 self.client_id
72 }
73
74 pub fn expected_nonce(&self) -> Option<&'a str> {
76 self.expected_nonce
77 }
78
79 pub fn user_info_url(&self) -> Option<&'a str> {
81 self.user_info_url
82 }
83
84 pub fn verified_claims(&self) -> Option<&VerifiedOAuthClaims<'a>> {
86 self.verified_claims.as_ref()
87 }
88}
89
90#[async_trait]
92pub trait GenericOAuthUserInfoHandler: Send + Sync {
93 async fn get_user_info(&self, tokens: &OAuthUserInfoRequest) -> AuthResult<Option<Value>>;
95
96 async fn get_user_info_with_context(
99 &self,
100 tokens: &OAuthUserInfoRequest,
101 _context: GenericOAuthProfileContext<'_>,
102 ) -> AuthResult<Option<Value>> {
103 self.get_user_info(tokens).await
104 }
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct OAuthProfile {
110 pub additional_fields: better_auth_core::FieldMap,
112 pub email: Option<better_auth_core::SchemaValue<Option<String>>>,
114 pub name: Option<better_auth_core::SchemaValue<Option<String>>>,
116 pub image: Option<Option<String>>,
118 pub email_verified: Option<better_auth_core::SchemaValue<Option<bool>>>,
120}
121
122#[async_trait]
124pub trait OAuthProfileMapper: Send + Sync {
125 async fn map_profile(&self, profile: &Value) -> AuthResult<OAuthProfile>;
127}
128
129#[async_trait]
131pub trait OAuthAccountSubject: Send + Sync {
132 async fn resolve_subject(
134 &self,
135 tokens: &OAuthUserInfoRequest,
136 profile: &Value,
137 ) -> AuthResult<String>;
138}
139
140#[async_trait]
142pub trait OAuthRefreshParameters: Send + Sync {
143 async fn parameters(&self, request: &AuthRequest) -> AuthResult<HashMap<String, String>>;
145}
146
147#[derive(Clone)]
149pub enum RefreshTokenParameters {
150 Static(HashMap<String, String>),
152 Dynamic(Arc<dyn OAuthRefreshParameters>),
154}
155
156#[derive(Clone)]
161pub struct GenericOAuthConfig {
162 pub client_id: String,
164 pub client_secret: Option<String>,
166 pub discovery_url: Option<String>,
168 pub discovery_headers: HeaderMap,
170 pub require_id_token_verification: bool,
172 pub authorization_url: Option<String>,
174 pub token_url: Option<String>,
176 pub user_info_url: Option<String>,
178 pub end_session_endpoint: Option<String>,
180 pub post_logout_redirect_uri: Option<String>,
182 pub disable_provider_logout: bool,
184 pub token_endpoint_auth: Option<TokenEndpointAuth>,
186 pub authentication: Option<TokenEndpointSecretAuthentication>,
188 pub scopes: Vec<String>,
190 pub redirect_uri: Option<String>,
192 pub response_type: Option<String>,
194 pub response_mode: Option<String>,
196 pub prompt: Option<String>,
198 pub pkce: bool,
200 pub access_type: Option<String>,
202 pub access_token_expires_in: Option<f64>,
204 pub authorization_url_params: IndexMap<String, String>,
206 pub token_url_params: HashMap<String, String>,
208 pub authorization_headers: HeaderMap,
210 pub refresh_token_params: Option<RefreshTokenParameters>,
212 pub get_token: Option<Arc<dyn OAuthTokenHandler>>,
214 pub get_user_info: Option<Arc<dyn GenericOAuthUserInfoHandler>>,
216 pub map_profile_to_user: Option<Arc<dyn OAuthProfileMapper>>,
218 pub account_subject: Option<Arc<dyn OAuthAccountSubject>>,
220 pub disable_implicit_sign_up: bool,
222 pub disable_sign_up: bool,
224 pub override_user_info: bool,
226 pub require_email_verification: bool,
228 pub allow_idp_initiated: bool,
230 pub disable_id_token_nonce_binding: bool,
232}
233
234impl Default for GenericOAuthConfig {
235 fn default() -> Self {
236 Self {
237 client_id: String::new(),
238 client_secret: None,
239 discovery_url: None,
240 discovery_headers: HeaderMap::new(),
241 require_id_token_verification: false,
242 authorization_url: None,
243 token_url: None,
244 user_info_url: None,
245 end_session_endpoint: None,
246 post_logout_redirect_uri: None,
247 disable_provider_logout: false,
248 token_endpoint_auth: None,
249 authentication: None,
250 scopes: Vec::new(),
251 redirect_uri: None,
252 response_type: None,
253 response_mode: None,
254 prompt: None,
255 pkce: true,
256 access_type: None,
257 access_token_expires_in: None,
258 authorization_url_params: IndexMap::new(),
259 token_url_params: HashMap::new(),
260 authorization_headers: HeaderMap::new(),
261 refresh_token_params: None,
262 get_token: None,
263 get_user_info: None,
264 map_profile_to_user: None,
265 account_subject: None,
266 disable_implicit_sign_up: false,
267 disable_sign_up: false,
268 override_user_info: false,
269 require_email_verification: false,
270 allow_idp_initiated: false,
271 disable_id_token_nonce_binding: false,
272 }
273 }
274}
275
276impl GenericOAuthConfig {
277 pub fn auth0(client_id: &str, client_secret: &str, domain: &str) -> AuthResult<Self> {
279 let address = if domain.starts_with("http://") || domain.starts_with("https://") {
280 domain.to_owned()
281 } else {
282 format!("https://{domain}")
283 };
284 let url = url::Url::parse(&address)
285 .map_err(|error| better_auth_core::AuthError::config(error.to_string()))?;
286 let host = &url[url::Position::BeforeHost..url::Position::AfterPort];
287 Ok(Self::discovery_preset(
288 client_id,
289 client_secret,
290 format!("https://{host}/.well-known/openid-configuration"),
291 ))
292 }
293
294 pub fn keycloak(client_id: &str, client_secret: &str, issuer: &str) -> Self {
296 Self::discovery_preset(
297 client_id,
298 client_secret,
299 format!(
300 "{}/.well-known/openid-configuration",
301 issuer.strip_suffix('/').unwrap_or(issuer)
302 ),
303 )
304 }
305
306 pub fn okta(client_id: &str, client_secret: &str, issuer: &str) -> Self {
308 Self::keycloak(client_id, client_secret, issuer)
309 }
310
311 fn discovery_preset(client_id: &str, client_secret: &str, discovery_url: String) -> Self {
312 Self {
313 client_id: client_id.into(),
314 client_secret: Some(client_secret.into()),
315 discovery_url: Some(discovery_url),
316 scopes: vec!["openid".into(), "profile".into(), "email".into()],
317 ..Default::default()
318 }
319 }
320}