Skip to main content

better_auth_api/plugins/oauth/
generic.rs

1use 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
12/// Authorization-code exchange values passed to a custom token handler.
13pub struct OAuthCodeExchange<'a> {
14    /// Authorization code returned by the provider.
15    pub code: &'a str,
16    /// Callback URI sent to the provider.
17    pub redirect_uri: &'a str,
18    /// PKCE verifier persisted with the authorization request.
19    pub code_verifier: Option<&'a str>,
20    /// Provider-specific device identifier from the callback.
21    pub device_id: Option<&'a str>,
22}
23
24/// Exchanges a code when a provider does not use a standard token endpoint.
25#[async_trait]
26pub trait OAuthTokenHandler: Send + Sync {
27    /// Exchange the authorization code without creating a local session.
28    async fn get_token(&self, request: OAuthCodeExchange<'_>) -> AuthResult<OAuthTokenSet>;
29}
30
31/// Borrowed claims from the configured OIDC signature and claim validation.
32pub 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    /// Read the claims returned by the configured OIDC signature and claim validation.
42    pub fn as_value(&self) -> &'a Value {
43        self.claims
44    }
45}
46
47/// Borrowed runtime inputs for a Generic OAuth profile lookup.
48pub 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    /// Read the client identifier from the resolved provider configuration.
70    pub fn client_id(&self) -> &'a str {
71        self.client_id
72    }
73
74    /// Read the nonce bound to this flow, when the caller supplied one.
75    pub fn expected_nonce(&self) -> Option<&'a str> {
76        self.expected_nonce
77    }
78
79    /// Read the resolved userinfo endpoint.
80    pub fn user_info_url(&self) -> Option<&'a str> {
81        self.user_info_url
82    }
83
84    /// Read claims that passed the configured OIDC signature and claim validation.
85    pub fn verified_claims(&self) -> Option<&VerifiedOAuthClaims<'a>> {
86        self.verified_claims.as_ref()
87    }
88}
89
90/// Fetches the raw profile used for Generic OAuth account recognition.
91#[async_trait]
92pub trait GenericOAuthUserInfoHandler: Send + Sync {
93    /// Return a raw provider profile, `None` when unavailable, or an application error.
94    async fn get_user_info(&self, tokens: &OAuthUserInfoRequest) -> AuthResult<Option<Value>>;
95
96    /// Read resolved options and completed verification before mapping a profile.
97    /// Existing handlers retain their token-based lookup unless they override this method.
98    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/// Local profile fields; provider account identity is resolved separately.
108#[derive(Debug, Clone, Default)]
109pub struct OAuthProfile {
110    /// Application user fields supplied by the profile mapper.
111    pub additional_fields: better_auth_core::FieldMap,
112    /// Leave the email unchanged with `None`, or override its string/null/undefined value.
113    pub email: Option<better_auth_core::SchemaValue<Option<String>>>,
114    /// Leave the name unchanged with `None`, or override its string/null/undefined value.
115    pub name: Option<better_auth_core::SchemaValue<Option<String>>>,
116    /// Override the profile image; `Some(None)` clears the provider value.
117    pub image: Option<Option<String>>,
118    /// Leave verification unchanged with `None`, or override its boolean/null/undefined value.
119    pub email_verified: Option<better_auth_core::SchemaValue<Option<bool>>>,
120}
121
122/// Maps provider profile fields without changing the provider account subject.
123#[async_trait]
124pub trait OAuthProfileMapper: Send + Sync {
125    /// Return local user fields derived from the raw provider profile.
126    async fn map_profile(&self, profile: &Value) -> AuthResult<OAuthProfile>;
127}
128
129/// Resolves a stable provider account identifier from the original profile.
130#[async_trait]
131pub trait OAuthAccountSubject: Send + Sync {
132    /// Return a nonempty immutable identifier; mapped local fields are not supplied.
133    async fn resolve_subject(
134        &self,
135        tokens: &OAuthUserInfoRequest,
136        profile: &Value,
137    ) -> AuthResult<String>;
138}
139
140/// Resolves extra token refresh parameters for the authenticated request.
141#[async_trait]
142pub trait OAuthRefreshParameters: Send + Sync {
143    /// Validate request-derived tenant or scope values before returning parameters.
144    async fn parameters(&self, request: &AuthRequest) -> AuthResult<HashMap<String, String>>;
145}
146
147/// Static or request-dependent parameters for a token refresh.
148#[derive(Clone)]
149pub enum RefreshTokenParameters {
150    /// Parameters shared by every refresh request.
151    Static(HashMap<String, String>),
152    /// Parameters computed from the authenticated request.
153    Dynamic(Arc<dyn OAuthRefreshParameters>),
154}
155
156/// Generic OAuth and OpenID Connect provider configuration.
157///
158/// Discovery resolves once when the auth instance is built. Set
159/// `require_id_token_verification` for providers whose identity comes from ID tokens.
160#[derive(Clone)]
161pub struct GenericOAuthConfig {
162    /// OAuth client identifier.
163    pub client_id: String,
164    /// Client secret; omit for public clients or private-key JWT authentication.
165    pub client_secret: Option<String>,
166    /// OpenID configuration URL.
167    pub discovery_url: Option<String>,
168    /// Headers included in the discovery request.
169    pub discovery_headers: HeaderMap,
170    /// Require discovery to supply a usable issuer and JWKS URI.
171    pub require_id_token_verification: bool,
172    /// Explicit authorization endpoint; overrides discovery.
173    pub authorization_url: Option<String>,
174    /// Explicit token endpoint; overrides discovery.
175    pub token_url: Option<String>,
176    /// Explicit userinfo endpoint; overrides discovery.
177    pub user_info_url: Option<String>,
178    /// Explicit RP-initiated logout endpoint; overrides discovery.
179    pub end_session_endpoint: Option<String>,
180    /// Default redirect after provider logout.
181    pub post_logout_redirect_uri: Option<String>,
182    /// Complete only local logout even when the provider supports logout.
183    pub disable_provider_logout: bool,
184    /// Client authentication for code exchange and token refresh.
185    pub token_endpoint_auth: Option<TokenEndpointAuth>,
186    /// Upstream's legacy secret authentication selection.
187    pub authentication: Option<TokenEndpointSecretAuthentication>,
188    /// Default scopes; OIDC discovery providers also request `openid`.
189    pub scopes: Vec<String>,
190    /// Override the generated callback URI.
191    pub redirect_uri: Option<String>,
192    /// Authorization response type; defaults to `code`.
193    pub response_type: Option<String>,
194    /// Authorization response mode, such as `query` or `form_post`.
195    pub response_mode: Option<String>,
196    /// Provider authentication prompt.
197    pub prompt: Option<String>,
198    /// Send PKCE challenges and verifiers; defaults to true.
199    pub pkce: bool,
200    /// Authorization access type, such as `offline`.
201    pub access_type: Option<String>,
202    /// Fallback access-token lifetime when the token response omits an expiry.
203    pub access_token_expires_in: Option<f64>,
204    /// Extra authorization parameters in insertion order; reserved fields are protected.
205    pub authorization_url_params: IndexMap<String, String>,
206    /// Extra code-exchange parameters that cannot replace standard grant fields.
207    pub token_url_params: HashMap<String, String>,
208    /// Additional headers for authorization-code exchange.
209    pub authorization_headers: HeaderMap,
210    /// Extra token refresh parameters.
211    pub refresh_token_params: Option<RefreshTokenParameters>,
212    /// Custom code exchange for providers with a nonstandard token endpoint.
213    pub get_token: Option<Arc<dyn OAuthTokenHandler>>,
214    /// Custom raw-profile lookup.
215    pub get_user_info: Option<Arc<dyn GenericOAuthUserInfoHandler>>,
216    /// Map profile fields independently of account recognition.
217    pub map_profile_to_user: Option<Arc<dyn OAuthProfileMapper>>,
218    /// Override the immutable subject resolver (`sub` for OIDC, `id` for OAuth).
219    pub account_subject: Option<Arc<dyn OAuthAccountSubject>>,
220    /// Require an explicit `requestSignUp` to create a new user.
221    pub disable_implicit_sign_up: bool,
222    /// Reject creation of new users through this provider.
223    pub disable_sign_up: bool,
224    /// Update existing local profile fields on sign-in.
225    pub override_user_info: bool,
226    /// Require the local user's email to be verified before issuing a session.
227    pub require_email_verification: bool,
228    /// Restart callbacks without state using a fresh authorization request.
229    pub allow_idp_initiated: bool,
230    /// Disable the default OIDC nonce binding for providers that cannot echo a nonce.
231    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    /// Configure Auth0 discovery from a domain or URL and the default OIDC scopes.
278    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    /// Configure Keycloak discovery from the realm issuer and default OIDC scopes.
295    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    /// Configure Okta discovery from the issuer and default OIDC scopes.
307    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}