Skip to main content

better_auth_api/plugins/oauth/providers/
constructors.rs

1use better_auth_core::{AuthError, AuthResult};
2
3use super::super::{CognitoOptions, GoogleOptions, TwitchOptions, google};
4use super::{OAuthProvider, ProviderKind};
5
6impl OAuthProvider {
7    /// Configure Social Microsoft with its tenant, authority, photo, and assertion options.
8    pub fn microsoft(
9        client_id: &str,
10        client_secret: &str,
11        options: super::microsoft::MicrosoftOptions,
12    ) -> AuthResult<Self> {
13        if !client_secret.is_empty() && options.client_assertion.is_some() {
14            return Err(AuthError::internal(
15                "Microsoft Entra ID clientAssertion cannot be combined with clientSecret",
16            ));
17        }
18        let authorization = format!(
19            "{}/{}/oauth2/v2.0/authorize",
20            options.authority(),
21            options.tenant()
22        );
23        let token = format!(
24            "{}/{}/oauth2/v2.0/token",
25            options.authority(),
26            options.tenant()
27        );
28        let token_endpoint_auth = options
29            .client_assertion
30            .clone()
31            .map(super::TokenEndpointAuth::PrivateKeyJwt);
32        Ok(Self {
33            kind: ProviderKind::Microsoft {
34                options,
35                token_endpoint_auth,
36            },
37            ..Self::custom(client_id, client_secret, &authorization, &token)
38        })
39    }
40
41    /// Configure a Cognito hosted domain and its fixed user-pool issuer and JWKS.
42    /// Set `identity_provider` through `authorization_params` to select a federated provider.
43    pub fn cognito(
44        client_id: &str,
45        client_secret: &str,
46        options: CognitoOptions,
47    ) -> AuthResult<Self> {
48        if options.domain.is_empty() || options.region.is_empty() || options.user_pool_id.is_empty()
49        {
50            better_auth_core::observability::logger::current().error(
51                "Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options.",
52                &[],
53            );
54            return Err(AuthError::internal("DOMAIN_AND_REGION_REQUIRED"));
55        }
56        let domain = options
57            .domain
58            .strip_prefix("https://")
59            .or_else(|| options.domain.strip_prefix("http://"))
60            .unwrap_or(&options.domain);
61        let provider = Self {
62            user_info_url: Some(format!("https://{domain}/oauth2/userinfo")),
63            ..Self::custom(
64                client_id,
65                client_secret,
66                &format!("https://{domain}/oauth2/authorize"),
67                &format!("https://{domain}/oauth2/token"),
68            )
69        };
70        Ok(Self {
71            kind: ProviderKind::Cognito(options),
72            ..provider
73        })
74    }
75
76    /// Configure a custom social provider with explicit endpoints and profile handling.
77    pub fn custom(client_id: &str, client_secret: &str, auth_url: &str, token_url: &str) -> Self {
78        Self {
79            kind: ProviderKind::Custom,
80            client_id: client_id.into(),
81            client_secret: client_secret.into(),
82            client_key: None,
83            auth_url: auth_url.into(),
84            token_url: token_url.into(),
85            token_endpoint_auth: None,
86            user_info_url: None,
87            redirect_uri: None,
88            end_session_endpoint: None,
89            post_logout_redirect_uri: None,
90            scopes: None,
91            disable_default_scope: false,
92            disable_id_token_sign_in: false,
93            prompt: None,
94            authorization_params: Vec::new(),
95            map_user_info: None,
96            map_profile_to_user: None,
97            get_user_info: None,
98            refresh_access_token: None,
99            verify_id_token: None,
100            disable_implicit_sign_up: None,
101            disable_sign_up: None,
102            require_email_verification: None,
103            override_user_info_on_sign_in: false,
104        }
105    }
106
107    /// Configure Google with its built-in endpoints and profile decoder.
108    pub fn google(client_id: &str, client_secret: &str) -> Self {
109        Self::google_with_options(client_id, client_secret, GoogleOptions::default())
110    }
111
112    /// Configure Google with additional ID-token audiences.
113    pub fn google_with_options(
114        client_id: &str,
115        client_secret: &str,
116        options: GoogleOptions,
117    ) -> Self {
118        Self {
119            kind: ProviderKind::Google {
120                jwks_url: google::JWKS_URL.to_owned(),
121                options,
122            },
123            user_info_url: Some("https://www.googleapis.com/oauth2/v3/userinfo".into()),
124            authorization_params: vec![("include_granted_scopes".into(), "true".into())],
125            ..Self::custom(
126                client_id,
127                client_secret,
128                "https://accounts.google.com/o/oauth2/v2/auth",
129                "https://oauth2.googleapis.com/token",
130            )
131        }
132    }
133
134    /// Configure GitHub with its built-in profile and email lookup.
135    pub fn github(client_id: &str, client_secret: &str) -> Self {
136        Self::github_with_endpoints(
137            client_id,
138            client_secret,
139            "https://github.com/login/oauth/authorize",
140            "https://github.com/login/oauth/access_token",
141            "https://api.github.com/user",
142            "https://api.github.com/user/emails",
143        )
144    }
145
146    /// Configure GitHub with custom endpoints while retaining GitHub profile semantics.
147    pub fn github_with_endpoints(
148        client_id: &str,
149        client_secret: &str,
150        auth_url: &str,
151        token_url: &str,
152        user_info_url: &str,
153        user_emails_url: &str,
154    ) -> Self {
155        Self {
156            kind: ProviderKind::GitHub {
157                user_url: user_info_url.into(),
158                emails_url: user_emails_url.into(),
159            },
160            user_info_url: Some(user_info_url.into()),
161            ..Self::custom(client_id, client_secret, auth_url, token_url)
162        }
163    }
164
165    /// Configure Discord with its built-in endpoints and profile decoder.
166    pub fn discord(client_id: &str, client_secret: &str) -> Self {
167        Self {
168            kind: ProviderKind::Discord,
169            user_info_url: Some("https://discord.com/api/users/@me".into()),
170            ..Self::custom(
171                client_id,
172                client_secret,
173                "https://discord.com/api/oauth2/authorize",
174                "https://discord.com/api/oauth2/token",
175            )
176        }
177    }
178
179    /// Configure GitLab with its built-in endpoints and active-user profile decoder.
180    pub fn gitlab(client_id: &str, client_secret: &str) -> Self {
181        Self::gitlab_with_issuer(client_id, client_secret, "")
182    }
183
184    /// Configure GitLab endpoints from an issuer, preserving its path prefix.
185    /// An empty issuer selects gitlab.com. Repeated path slashes are collapsed.
186    pub fn gitlab_with_issuer(client_id: &str, client_secret: &str, issuer: &str) -> Self {
187        let issuer = if issuer.is_empty() {
188            "https://gitlab.com"
189        } else {
190            issuer
191        };
192        let endpoint = |path| {
193            format!("{issuer}/{path}")
194                .split("://")
195                .map(|segment| {
196                    let mut previous_slash = false;
197                    segment
198                        .chars()
199                        .filter(|&character| {
200                            let repeated = previous_slash && character == '/';
201                            previous_slash = character == '/';
202                            !repeated
203                        })
204                        .collect::<String>()
205                })
206                .collect::<Vec<_>>()
207                .join("://")
208        };
209        Self {
210            kind: ProviderKind::GitLab,
211            user_info_url: Some(endpoint("api/v4/user")),
212            ..Self::custom(
213                client_id,
214                client_secret,
215                &endpoint("oauth/authorize"),
216                &endpoint("oauth/token"),
217            )
218        }
219    }
220
221    /// Configure Spotify with its built-in endpoints and profile decoder.
222    pub fn spotify(client_id: &str, client_secret: &str) -> Self {
223        Self {
224            kind: ProviderKind::Spotify,
225            user_info_url: Some("https://api.spotify.com/v1/me".into()),
226            ..Self::custom(
227                client_id,
228                client_secret,
229                "https://accounts.spotify.com/authorize",
230                "https://accounts.spotify.com/api/token",
231            )
232        }
233    }
234
235    /// Configure Hugging Face with its built-in endpoints and HTTP profile decoder.
236    pub fn huggingface(client_id: &str, client_secret: &str) -> Self {
237        Self {
238            kind: ProviderKind::HuggingFace,
239            user_info_url: Some("https://huggingface.co/oauth/userinfo".into()),
240            ..Self::custom(
241                client_id,
242                client_secret,
243                "https://huggingface.co/oauth/authorize",
244                "https://huggingface.co/oauth/token",
245            )
246        }
247    }
248
249    /// Configure Polar with its built-in endpoints and HTTP profile decoder.
250    pub fn polar(client_id: &str, client_secret: &str) -> Self {
251        Self {
252            kind: ProviderKind::Polar,
253            user_info_url: Some("https://api.polar.sh/v1/oauth2/userinfo".into()),
254            ..Self::custom(
255                client_id,
256                client_secret,
257                "https://polar.sh/oauth2/authorize",
258                "https://api.polar.sh/v1/oauth2/token",
259            )
260        }
261    }
262
263    /// Configure Vercel with its built-in endpoints and HTTP profile decoder.
264    pub fn vercel(client_id: &str, client_secret: &str) -> Self {
265        Self {
266            kind: ProviderKind::Vercel,
267            user_info_url: Some("https://api.vercel.com/login/oauth/userinfo".into()),
268            ..Self::custom(
269                client_id,
270                client_secret,
271                "https://vercel.com/oauth/authorize",
272                "https://api.vercel.com/login/oauth/token",
273            )
274        }
275    }
276
277    /// Configure Figma with PKCE, HTTP Basic token authentication, and its HTTP profile decoder.
278    pub fn figma(client_id: &str, client_secret: &str) -> Self {
279        Self {
280            kind: ProviderKind::Figma,
281            user_info_url: Some("https://api.figma.com/v1/me".into()),
282            ..Self::custom(
283                client_id,
284                client_secret,
285                "https://www.figma.com/oauth",
286                "https://api.figma.com/v1/oauth/token",
287            )
288        }
289    }
290
291    /// Configure Dropbox with PKCE, client-secret-post authentication, and its POST profile lookup.
292    pub fn dropbox(client_id: &str, client_secret: &str) -> Self {
293        Self {
294            kind: ProviderKind::Dropbox,
295            user_info_url: Some("https://api.dropboxapi.com/2/users/get_current_account".into()),
296            ..Self::custom(
297                client_id,
298                client_secret,
299                "https://www.dropbox.com/oauth2/authorize",
300                "https://api.dropboxapi.com/oauth2/token",
301            )
302        }
303    }
304
305    /// Configure Kick with PKCE, client-secret-post, and its HTTP profile decoder.
306    pub fn kick(client_id: &str, client_secret: &str) -> Self {
307        Self {
308            kind: ProviderKind::Kick,
309            user_info_url: Some("https://api.kick.com/public/v1/users".into()),
310            ..Self::custom(
311                client_id,
312                client_secret,
313                "https://id.kick.com/oauth/authorize",
314                "https://id.kick.com/oauth/token",
315            )
316        }
317    }
318
319    /// Configure LinkedIn with its code flow, client-secret-post, and Bearer HTTP profile.
320    /// LinkedIn omits PKCE parameters in the pinned provider protocol.
321    pub fn linkedin(client_id: &str, client_secret: &str) -> Self {
322        Self {
323            kind: ProviderKind::LinkedIn,
324            user_info_url: Some("https://api.linkedin.com/v2/userinfo".into()),
325            ..Self::custom(
326                client_id,
327                client_secret,
328                "https://www.linkedin.com/oauth/v2/authorization",
329                "https://www.linkedin.com/oauth/v2/accessToken",
330            )
331        }
332    }
333
334    /// Configure Slack with its code flow, client-secret-post, and OpenID HTTP profile.
335    pub fn slack(client_id: &str, client_secret: &str) -> Self {
336        Self {
337            kind: ProviderKind::Slack,
338            user_info_url: Some("https://slack.com/api/openid.connect.userInfo".into()),
339            ..Self::custom(
340                client_id,
341                client_secret,
342                "https://slack.com/openid/connect/authorize",
343                "https://slack.com/api/openid.connect.token",
344            )
345        }
346    }
347
348    /// Configure Naver with its code flow, client-secret-post, and HTTP profile envelope.
349    pub fn naver(client_id: &str, client_secret: &str) -> Self {
350        Self {
351            kind: ProviderKind::Naver,
352            user_info_url: Some("https://openapi.naver.com/v1/nid/me".into()),
353            ..Self::custom(
354                client_id,
355                client_secret,
356                "https://nid.naver.com/oauth2.0/authorize",
357                "https://nid.naver.com/oauth2.0/token",
358            )
359        }
360    }
361
362    /// Configure Social LINE with PKCE and its remote direct ID-token verifier.
363    pub fn line(client_id: &str, client_secret: &str) -> Self {
364        Self {
365            kind: ProviderKind::Line {
366                verify_url: "https://api.line.me/oauth2/v2.1/verify".into(),
367            },
368            user_info_url: Some("https://api.line.me/oauth2/v2.1/userinfo".into()),
369            ..Self::custom(
370                client_id,
371                client_secret,
372                "https://access.line.me/oauth2/v2.1/authorize",
373                "https://api.line.me/oauth2/v2.1/token",
374            )
375        }
376    }
377
378    /// Configure Linear with its code flow, client-secret-post, and GraphQL viewer profile.
379    pub fn linear(client_id: &str, client_secret: &str) -> Self {
380        Self {
381            kind: ProviderKind::Linear,
382            user_info_url: Some("https://api.linear.app/graphql".into()),
383            ..Self::custom(
384                client_id,
385                client_secret,
386                "https://linear.app/oauth/authorize",
387                "https://api.linear.app/oauth/token",
388            )
389        }
390    }
391
392    /// Configure Atlassian with PKCE, client-secret-post, and its HTTP account profile.
393    pub fn atlassian(client_id: &str, client_secret: &str) -> Self {
394        Self {
395            kind: ProviderKind::Atlassian,
396            user_info_url: Some("https://api.atlassian.com/me".into()),
397            ..Self::custom(
398                client_id,
399                client_secret,
400                "https://auth.atlassian.com/authorize",
401                "https://auth.atlassian.com/oauth/token",
402            )
403        }
404    }
405
406    /// Configure Salesforce production with PKCE and its HTTP user-info profile.
407    /// Set `auth_url`, `token_url`, and `user_info_url` together for a sandbox or custom login host.
408    pub fn salesforce(client_id: &str, client_secret: &str) -> Self {
409        Self {
410            kind: ProviderKind::Salesforce,
411            user_info_url: Some("https://login.salesforce.com/services/oauth2/userinfo".into()),
412            ..Self::custom(
413                client_id,
414                client_secret,
415                "https://login.salesforce.com/services/oauth2/authorize",
416                "https://login.salesforce.com/services/oauth2/token",
417            )
418        }
419    }
420
421    /// Configure Reddit with HTTP Basic token authentication and its API user profile.
422    /// Set `duration` to `permanent` through `authorization_params` for persistent access.
423    pub fn reddit(client_id: &str, client_secret: &str) -> Self {
424        Self {
425            kind: ProviderKind::Reddit,
426            user_info_url: Some("https://oauth.reddit.com/api/v1/me".into()),
427            ..Self::custom(
428                client_id,
429                client_secret,
430                "https://www.reddit.com/api/v1/authorize",
431                "https://www.reddit.com/api/v1/access_token",
432            )
433        }
434    }
435
436    /// Configure Kakao with ordered scopes, client-secret-post, and its account profile.
437    pub fn kakao(client_id: &str, client_secret: &str) -> Self {
438        Self {
439            kind: ProviderKind::Kakao,
440            user_info_url: Some("https://kapi.kakao.com/v2/user/me".into()),
441            ..Self::custom(
442                client_id,
443                client_secret,
444                "https://kauth.kakao.com/oauth/authorize",
445                "https://kauth.kakao.com/oauth/token",
446            )
447        }
448    }
449
450    /// Configure Zoom with authorization PKCE, client-secret-post, and its HTTP user profile.
451    /// The pinned Zoom provider ignores configured and request scopes.
452    pub fn zoom(client_id: &str, client_secret: &str) -> Self {
453        Self {
454            kind: ProviderKind::Zoom { pkce: true },
455            user_info_url: Some("https://api.zoom.us/v2/users/me".into()),
456            ..Self::custom(
457                client_id,
458                client_secret,
459                "https://zoom.us/oauth/authorize",
460                "https://zoom.us/oauth/token",
461            )
462        }
463    }
464
465    /// Configure Zoom with its `pkce: false` option for the authorization URL.
466    /// Code exchange still forwards the supplied verifier, matching the pinned provider.
467    pub fn zoom_without_pkce(client_id: &str, client_secret: &str) -> Self {
468        Self {
469            kind: ProviderKind::Zoom { pkce: false },
470            ..Self::zoom(client_id, client_secret)
471        }
472    }
473
474    /// Configure Twitter with PKCE, HTTP Basic token authentication, and its two user-info requests.
475    pub fn twitter(client_id: &str, client_secret: &str) -> Self {
476        Self {
477            kind: ProviderKind::Twitter,
478            user_info_url: Some(
479                "https://api.x.com/2/users/me?user.fields=profile_image_url".into(),
480            ),
481            ..Self::custom(
482                client_id,
483                client_secret,
484                "https://x.com/i/oauth2/authorize",
485                "https://api.x.com/2/oauth2/token",
486            )
487        }
488    }
489
490    /// Configure WeChat WebsiteApp with GET grants and its HTTP profile lookup.
491    /// Set `authorization_params`'s `lang` to `en` for the English authorization page.
492    pub fn wechat(client_id: &str, client_secret: &str) -> Self {
493        Self::wechat_with_endpoints(
494            client_id,
495            client_secret,
496            "https://open.weixin.qq.com/connect/qrconnect",
497            "https://api.weixin.qq.com/sns/oauth2/access_token",
498            "https://api.weixin.qq.com/sns/oauth2/refresh_token",
499            "https://api.weixin.qq.com/sns/userinfo",
500        )
501    }
502
503    /// Configure WeChat endpoints while retaining its GET grants and profile semantics.
504    pub fn wechat_with_endpoints(
505        client_id: &str,
506        client_secret: &str,
507        auth_url: &str,
508        token_url: &str,
509        refresh_url: &str,
510        user_info_url: &str,
511    ) -> Self {
512        Self {
513            kind: ProviderKind::WeChat {
514                refresh_url: refresh_url.into(),
515            },
516            user_info_url: Some(user_info_url.into()),
517            ..Self::custom(client_id, client_secret, auth_url, token_url)
518        }
519    }
520
521    /// Configure VK with PKCE, client-secret-post grants, and form-based user info.
522    pub fn vk(client_id: &str, client_secret: &str) -> Self {
523        Self {
524            kind: ProviderKind::Vk,
525            user_info_url: Some("https://id.vk.com/oauth2/user_info".into()),
526            ..Self::custom(
527                client_id,
528                client_secret,
529                "https://id.vk.com/authorize",
530                "https://id.vk.com/oauth2/auth",
531            )
532        }
533    }
534
535    /// Configure Twitch with requested ID-token claims and client-secret-post grants.
536    pub fn twitch(client_id: &str, client_secret: &str, options: TwitchOptions) -> Self {
537        Self {
538            kind: ProviderKind::Twitch(options),
539            ..Self::custom(
540                client_id,
541                client_secret,
542                "https://id.twitch.tv/oauth2/authorize",
543                "https://id.twitch.tv/oauth2/token",
544            )
545        }
546    }
547
548    /// Configure Notion with its user-owned integration and owner profile.
549    pub fn notion(client_id: &str, client_secret: &str) -> Self {
550        Self {
551            kind: ProviderKind::Notion,
552            user_info_url: Some("https://api.notion.com/v1/users/me".into()),
553            ..Self::custom(
554                client_id,
555                client_secret,
556                "https://api.notion.com/v1/oauth/authorize",
557                "https://api.notion.com/v1/oauth/token",
558            )
559        }
560    }
561
562    /// Configure Roblox with its profile scopes, account prompt, and HTTP user profile.
563    pub fn roblox(client_id: &str, client_secret: &str) -> Self {
564        Self {
565            kind: ProviderKind::Roblox,
566            user_info_url: Some("https://apis.roblox.com/oauth/v1/userinfo".into()),
567            ..Self::custom(
568                client_id,
569                client_secret,
570                "https://apis.roblox.com/oauth/v1/authorize",
571                "https://apis.roblox.com/oauth/v1/token",
572            )
573        }
574    }
575
576    /// Configure TikTok with a client key, comma-separated scopes, and its HTTP profile.
577    /// TikTok sends the client key and secret in both token grants.
578    pub fn tiktok(client_key: &str, client_secret: &str) -> Self {
579        Self {
580            kind: ProviderKind::TikTok,
581            client_key: Some(client_key.into()),
582            user_info_url: Some("https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_large_url,display_name,username".into()),
583            ..Self::custom(
584                "",
585                client_secret,
586                "https://www.tiktok.com/v2/auth/authorize",
587                "https://open.tiktokapis.com/v2/oauth/token/",
588            )
589        }
590    }
591
592    /// Configure Railway with PKCE, HTTP Basic token authentication, and its OpenID profile.
593    pub fn railway(client_id: &str, client_secret: &str) -> Self {
594        Self {
595            kind: ProviderKind::Railway,
596            user_info_url: Some("https://backboard.railway.com/oauth/me".into()),
597            ..Self::custom(
598                client_id,
599                client_secret,
600                "https://backboard.railway.com/oauth/auth",
601                "https://backboard.railway.com/oauth/token",
602            )
603        }
604    }
605
606    /// Configure Cloudflare with PKCE and its API user profile.
607    /// Pass an empty secret for a public client; confidential clients default to HTTP Basic.
608    pub fn cloudflare(client_id: &str, client_secret: &str) -> Self {
609        Self {
610            kind: ProviderKind::Cloudflare,
611            user_info_url: Some("https://api.cloudflare.com/client/v4/user".into()),
612            ..Self::custom(
613                client_id,
614                client_secret,
615                "https://dash.cloudflare.com/oauth2/auth",
616                "https://dash.cloudflare.com/oauth2/token",
617            )
618        }
619    }
620}