better_auth_api/plugins/oauth/
mod.rs1use async_trait::async_trait;
2use std::{collections::HashMap, sync::Arc};
3
4use super::email_verification::EmailVerificationPlugin;
5use tokio::sync::OnceCell;
6
7use better_auth_core::AuthResult;
8use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
9use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
10
11mod account;
12#[cfg(test)]
13mod apple_client_secret_tests;
14#[cfg(test)]
15mod apple_flow_tests;
16#[cfg(test)]
17mod apple_tests;
18pub use providers::apple::{AppleClientSecretError, AppleClientSecretSigner, AppleOptions};
19mod authorization;
20mod callback;
21mod callbacks;
22pub use callbacks::{OAuthCallbacks, OAuthVerifierFuture};
23mod cognito;
24#[cfg(test)]
25mod cognito_contract_tests;
26#[cfg(test)]
27mod cognito_signed_tests;
28pub use cognito::CognitoOptions;
29pub mod encryption;
30#[cfg(test)]
31mod facebook_contract_tests;
32#[cfg(test)]
33mod facebook_signed_tests;
34mod generic;
35mod generic_presets;
36mod generic_profile;
37#[cfg(all(test, feature = "axum"))]
38mod gitlab_issuer_tests;
39pub(super) mod google;
40pub use google::GoogleOptions;
41#[cfg(all(test, feature = "axum"))]
42mod google_client_ids_tests;
43mod handlers;
44mod id_token;
45pub use providers::facebook::FacebookOptions;
46pub use providers::microsoft::{MicrosoftOptions, MicrosoftProfilePhotoSize};
47#[cfg(all(test, feature = "axum"))]
48mod microsoft_tests;
49pub use providers::twitch::TwitchOptions;
50mod line;
51mod microsoft_entra;
52pub(crate) use handlers::validate_redirect_target;
53pub(crate) use signin::sign_in_verified_profile;
54mod logout;
55pub(super) use logout::handle_sign_out;
56mod oidc;
57#[cfg(all(test, feature = "axum"))]
58mod paybin_tests;
59#[cfg(test)]
60mod paypal_tests;
61mod popup;
62mod provider_tokens;
63mod providers;
64pub use popup::{OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupPlugin};
65mod proxy;
66pub use proxy::{OAuthProxyConfig, OAuthProxyPlugin};
67mod request;
68mod resolved;
69#[cfg(test)]
70mod salesforce_tests;
71mod signin;
72#[cfg(test)]
73mod social_nonce_tests;
74mod social_profile;
75#[cfg(all(test, feature = "axum"))]
76mod social_token_wire_tests;
77mod state;
78mod state_json;
79#[cfg(all(test, feature = "axum"))]
80mod tiktok_tests;
81mod token;
82#[cfg(test)]
83mod twitter_tests;
84mod types;
85#[cfg(test)]
86mod vk_tests;
87#[cfg(test)]
88mod wechat_tests;
89
90#[cfg(test)]
91mod google_test_support;
92#[cfg(test)]
93mod google_tests;
94#[cfg(test)]
95mod verifier_context_tests;
96
97pub use better_auth_core::NativeRequest;
98pub use generic::{
99 GenericOAuthConfig, GenericOAuthProfileContext, GenericOAuthUserInfoHandler,
100 OAuthAccountSubject, OAuthCodeExchange, OAuthProfile, OAuthProfileMapper,
101 OAuthRefreshParameters, OAuthTokenHandler, RefreshTokenParameters, VerifiedOAuthClaims,
102};
103pub use token::{
104 ClientAssertion, ClientAssertionContext, TokenEndpointAuth, TokenEndpointRequestContext,
105 TokenEndpointSecretAuthentication, TokenGrantType, TokenRequestHook,
106};
107
108pub use providers::{
109 OAuthCallbackUserName, OAuthCallbackUserPayload, OAuthConfig, OAuthIdTokenVerifier,
110 OAuthProvider, OAuthRefreshTokenHandler, OAuthTokenSet, OAuthUserInfo, OAuthUserInfoHandler,
111 OAuthUserInfoRequest, OAuthUserInfoResponse,
112};
113
114pub struct OAuthPlugin {
115 config: OAuthConfig,
116 generic: HashMap<String, GenericOAuthConfig>,
117 email_verification: Option<Arc<EmailVerificationPlugin>>,
118 resolved: OnceCell<Arc<resolved::ResolvedOAuthConfig>>,
119}
120
121impl OAuthPlugin {
122 pub fn new() -> Self {
123 Self {
124 config: OAuthConfig::default(),
125 generic: HashMap::new(),
126 email_verification: None,
127 resolved: OnceCell::new(),
128 }
129 }
130
131 pub fn with_config(config: OAuthConfig) -> Self {
132 Self {
133 config,
134 ..Self::new()
135 }
136 }
137
138 pub fn add_provider(mut self, name: &str, provider: OAuthProvider) -> Self {
139 let _ = self.config.providers.insert(name.to_string(), provider);
140 self.resolved = OnceCell::new();
141 self
142 }
143
144 pub fn add_generic_provider(mut self, name: &str, config: GenericOAuthConfig) -> Self {
146 let _ = self.generic.insert(name.to_owned(), config);
147 self.resolved = OnceCell::new();
148 self
149 }
150
151 pub fn with_email_verification(mut self, plugin: Arc<EmailVerificationPlugin>) -> Self {
153 self.email_verification = Some(plugin);
154 self.resolved = OnceCell::new();
155 self
156 }
157
158 pub async fn has_provider(&self, name: &str) -> AuthResult<bool> {
165 Ok(self.resolved_config().await?.providers.contains_key(name))
166 }
167
168 async fn resolved_config(&self) -> AuthResult<&Arc<resolved::ResolvedOAuthConfig>> {
169 self.resolved
170 .get_or_try_init(|| async {
171 resolved::ResolvedOAuthConfig::new(
172 &self.config,
173 &self.generic,
174 self.email_verification.clone(),
175 )
176 .await
177 .map(Arc::new)
178 })
179 .await
180 }
181}
182
183impl Default for OAuthPlugin {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[async_trait]
190impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for OAuthPlugin {
191 fn name(&self) -> &'static str {
192 "oauth"
193 }
194
195 fn telemetry_plugin_id(&self) -> Option<&'static str> {
196 (!self.generic.is_empty()).then_some("generic-oauth")
197 }
198
199 fn telemetry(&self, options: &mut better_auth_core::observability::telemetry::PluginTelemetry) {
200 use better_auth_core::observability::telemetry::SocialProviderTelemetry;
201 if let Some(verification) = &self.email_verification {
202 <EmailVerificationPlugin as AuthPlugin<S>>::telemetry(verification, options);
203 }
204 options
205 .social_providers
206 .extend(
207 self.config
208 .providers
209 .iter()
210 .map(|(id, provider)| SocialProviderTelemetry {
211 id: id.clone(),
212 map_profile_to_user: provider.map_profile_to_user.is_some(),
213 disable_default_scope: provider.disable_default_scope,
214 disable_id_token_sign_in: provider.disable_id_token_sign_in,
215 disable_implicit_sign_up: provider.disable_implicit_sign_up,
216 disable_sign_up: provider.disable_sign_up,
217 get_user_info: provider.get_user_info.is_some(),
218 override_user_info_on_sign_in: provider.override_user_info_on_sign_in,
219 prompt: provider.prompt.clone(),
220 verify_id_token: provider.verify_id_token.is_some(),
221 scope: provider.scopes.clone(),
222 refresh_access_token: provider.refresh_access_token.is_some(),
223 }),
224 );
225 }
226
227 async fn on_init(&self, ctx: &mut better_auth_core::AuthInitContext<S>) -> AuthResult<()> {
228 let resolved = self.resolved_config().await?;
229 if let Some(callbacks) = ctx.extensions.get::<Arc<OAuthCallbacks<S>>>() {
230 for name in callbacks.verifiers.keys() {
231 if !resolved.providers.contains_key(name) {
232 return Err(better_auth_core::AuthError::config(format!(
233 "OAuth verifier provider {name} is not registered"
234 )));
235 }
236 }
237 }
238 ctx.extensions.insert(self.config.clone());
239 ctx.extensions.insert(resolved.clone());
240 Ok(())
241 }
242
243 fn routes(&self) -> Vec<AuthRoute> {
244 vec![
245 AuthRoute::post("/sign-in/social", "socialSignIn").body_validator(request::validate),
246 AuthRoute::get("/callback/{id}", "callbackOAuth")
247 .body_validator(callback::body)
248 .query_validator(crate::plugins::query_input::callback)
249 .allowed_media_types(&["application/x-www-form-urlencoded", "application/json"]),
250 AuthRoute::post("/callback/{id}", "callbackOAuth")
251 .body_validator(callback::body)
252 .query_validator(crate::plugins::query_input::callback)
253 .allowed_media_types(&["application/x-www-form-urlencoded", "application/json"]),
254 AuthRoute::post("/link-social", "linkSocialAccount")
255 .require_headers(true)
256 .body_validator(request::validate),
257 AuthRoute::post("/get-access-token", "getAccessToken")
258 .body_validator(account::account_body),
259 AuthRoute::post("/refresh-token", "refreshToken").body_validator(account::account_body),
260 AuthRoute::get("/account-info", "accountInfo")
261 .query_validator(crate::plugins::query_input::account_selection),
262 ]
263 }
264
265 async fn on_request(
266 &self,
267 req: &AuthRequest,
268 ctx: &AuthContext<S>,
269 ) -> AuthResult<Option<AuthResponse>> {
270 let config = self.resolved_config().await?;
271 match (req.method(), req.path()) {
272 (HttpMethod::Post, "/sign-in/social") => Ok(Some(
273 handlers::handle_social_sign_in(config, req, ctx).await?,
274 )),
275 (HttpMethod::Get | HttpMethod::Post, path) if path_matches_callback(path) => {
276 let provider = extract_provider_from_callback(path);
277 Ok(Some(
278 callback::handle_callback(config, &provider, req, ctx).await?,
279 ))
280 }
281 (HttpMethod::Post, "/link-social") => {
282 Ok(Some(handlers::handle_link_social(config, req, ctx).await?))
283 }
284 (HttpMethod::Post, "/get-access-token") => Ok(Some(
285 account::handle_get_access_token(config, req, ctx).await?,
286 )),
287 (HttpMethod::Post, "/refresh-token") => {
288 Ok(Some(account::handle_refresh_token(config, req, ctx).await?))
289 }
290 (HttpMethod::Get, "/account-info") => {
291 Ok(Some(account::handle_account_info(config, req, ctx).await?))
292 }
293 _ => Ok(None),
294 }
295 }
296}
297
298fn path_matches_callback(path: &str) -> bool {
300 let path_without_query = path.split('?').next().unwrap_or(path);
301 path_without_query
302 .strip_prefix("/callback/")
303 .is_some_and(|provider| !provider.is_empty() && !provider.contains('/'))
304}
305
306fn extract_provider_from_callback(path: &str) -> String {
308 let path_without_query = path.split('?').next().unwrap_or(path);
309 path_without_query["/callback/".len()..].to_string()
310}
311
312#[cfg(test)]
313mod generic_signin_tests;
314
315#[cfg(test)]
316mod readiness_tests;
317
318#[cfg(test)]
319mod social_options_tests;
320
321#[cfg(test)]
322mod email_verified_tests;
323
324#[cfg(test)]
325mod http_provider_tests;