Skip to main content

rustauth_plugins/generic_oauth/
mod.rs

1//! Generic OAuth plugin support.
2
3mod account;
4mod config;
5mod discovery;
6mod errors;
7mod provider;
8pub mod providers;
9mod route_http;
10mod route_support;
11mod routes;
12mod user_info;
13
14use rustauth_core::plugin::{AuthPlugin, PluginInitOutput};
15use rustauth_oauth::oauth2::SocialOAuthProvider;
16use std::collections::BTreeSet;
17use std::sync::Arc;
18
19pub const UPSTREAM_PLUGIN_ID: &str = "generic-oauth";
20
21pub use config::{
22    GenericOAuthConfig, GenericOAuthFlow, GenericOAuthGetToken, GenericOAuthGetUserInfo,
23    GenericOAuthMapProfileToUser, GenericOAuthOptions, GenericOAuthOptionsBuilder,
24    GenericOAuthParams, GenericOAuthParamsCallback, GenericOAuthParamsContext,
25    GenericOAuthParamsFuture, GenericOAuthRefreshAccessToken, GenericOAuthRevokeToken,
26    GenericOAuthTokenRequest, GenericOAuthVerifyIdToken,
27};
28pub use errors::{
29    INVALID_OAUTH_CONFIG, INVALID_OAUTH_CONFIGURATION, ISSUER_MISMATCH, ISSUER_MISSING,
30    PROVIDER_CONFIG_NOT_FOUND, PROVIDER_ID_REQUIRED, SESSION_REQUIRED, TOKEN_URL_NOT_FOUND,
31};
32pub use provider::GenericOAuthProvider;
33pub use providers::{
34    auth0, gumroad, hubspot, keycloak, line, microsoft_entra_id, okta, patreon, slack,
35    Auth0Options, BaseOAuthProviderOptions, GumroadOptions, HubSpotOptions, KeycloakOptions,
36    LineOptions, MicrosoftEntraIdOptions, OktaOptions, PatreonOptions, SlackOptions,
37};
38
39/// Build the Better Auth-compatible generic OAuth plugin.
40#[must_use]
41pub fn generic_oauth(options: GenericOAuthOptions) -> AuthPlugin {
42    let init_options = options.clone();
43    let discovery_cache = discovery::DiscoveryCache::default();
44    let init_discovery_cache = discovery_cache.clone();
45    let plugin = AuthPlugin::new(UPSTREAM_PLUGIN_ID)
46        .with_version(env!("CARGO_PKG_VERSION"))
47        .with_options(options.to_json())
48        .with_error_code(errors::error_code(
49            INVALID_OAUTH_CONFIGURATION,
50            "Invalid OAuth configuration",
51        ))
52        .with_error_code(errors::error_code(
53            TOKEN_URL_NOT_FOUND,
54            "Invalid OAuth configuration. Token URL not found.",
55        ))
56        .with_error_code(errors::error_code(
57            PROVIDER_CONFIG_NOT_FOUND,
58            "No config found for provider",
59        ))
60        .with_error_code(errors::error_code(
61            PROVIDER_ID_REQUIRED,
62            "Provider ID is required",
63        ))
64        .with_error_code(errors::error_code(
65            INVALID_OAUTH_CONFIG,
66            "Invalid OAuth configuration.",
67        ))
68        .with_error_code(errors::error_code(SESSION_REQUIRED, "Session is required"))
69        .with_error_code(errors::error_code(
70            ISSUER_MISMATCH,
71            "OAuth issuer mismatch. The authorization server issuer does not match the expected value (RFC 9207).",
72        ))
73        .with_error_code(errors::error_code(
74            ISSUER_MISSING,
75            "OAuth issuer parameter missing. The authorization server did not include the required iss parameter (RFC 9207).",
76        ))
77        .with_endpoint(routes::sign_in_oauth2_endpoint(
78            options.clone(),
79            discovery_cache.clone(),
80        ))
81        .with_endpoint(routes::oauth2_callback_endpoint(
82            options.clone(),
83            discovery_cache.clone(),
84        ))
85        .with_endpoint(routes::oauth2_link_endpoint(options, discovery_cache));
86
87    plugin.with_init(move |_context| {
88        let mut output = PluginInitOutput::new();
89        let mut seen = BTreeSet::new();
90        for config in &init_options.config {
91            if !seen.insert(config.provider_id.clone()) {
92                continue;
93            }
94            let provider: Arc<dyn SocialOAuthProvider> =
95                Arc::new(GenericOAuthProvider::with_discovery_cache(
96                    config.clone(),
97                    init_discovery_cache.clone(),
98                ));
99            output = output.social_provider(provider);
100        }
101        Ok(output)
102    })
103}