Skip to main content

openauth_oauth_provider/
lib.rs

1//! OAuth 2.1 and OpenID Connect provider support for OpenAuth.
2//!
3//! This crate ports the server-side Better Auth `oauth-provider` behavior into
4//! idiomatic Rust. It is intentionally separate from `openauth-oauth`, which
5//! contains OAuth client and social-provider primitives.
6
7mod authorize;
8mod client;
9mod consent;
10mod endpoints;
11mod error;
12mod metadata;
13mod models;
14mod options;
15mod schema;
16mod token;
17mod utils;
18
19pub mod mcp;
20
21pub use authorize::{decide_authorize, AuthorizeDecision};
22pub use client::{
23    check_oauth_client, oauth_to_schema, schema_to_oauth, CreateOAuthClientInput, OAuthClient,
24};
25pub use consent::{
26    delete_consent, find_consent, has_granted_scopes, upsert_consent, ConsentGrantInput,
27};
28pub use error::OAuthProviderError;
29pub use metadata::{auth_server_metadata, oidc_server_metadata};
30pub use models::{OAuthAccessToken, OAuthConsent, OAuthRefreshToken, SchemaClient};
31pub use options::{
32    GrantType, OAuthProviderConfigError, OAuthProviderOptions, OAuthProviderPlugin,
33    ResolvedOAuthProviderOptions, SecretStorage, TokenEndpointAuthMethod,
34};
35pub use schema::{
36    oauth_provider_schema, OAUTH_ACCESS_TOKEN_MODEL, OAUTH_CLIENT_MODEL, OAUTH_CONSENT_MODEL,
37    OAUTH_REFRESH_TOKEN_MODEL,
38};
39pub use token::{
40    create_client_credentials_token, decode_refresh_token, store_client_secret, store_token,
41    verify_client_secret, TokenResponse,
42};
43
44use std::collections::HashSet;
45use std::sync::Arc;
46
47use openauth_core::options::RateLimitRule;
48use openauth_core::plugin::{AuthPlugin, PluginRateLimitRule};
49
50/// Current crate version.
51pub const VERSION: &str = env!("CARGO_PKG_VERSION");
52
53/// Build the OAuth provider extension.
54pub fn oauth_provider(
55    options: OAuthProviderOptions,
56) -> Result<OAuthProviderPlugin, OAuthProviderConfigError> {
57    let resolved = resolve_options(options)?;
58    let shared = Arc::new(resolved.clone());
59    let mut auth_plugin = AuthPlugin::new("oauth-provider").with_version(VERSION);
60
61    if !resolved.disable_jwt_plugin {
62        let jwt_plugin = openauth_plugins::jwt::jwt()
63            .map_err(|error| OAuthProviderConfigError::JwtPlugin(error.to_string()))?;
64        auth_plugin.schema.extend(jwt_plugin.schema);
65        auth_plugin.endpoints.extend(jwt_plugin.endpoints);
66        auth_plugin.migrations.extend(jwt_plugin.migrations);
67        auth_plugin.database_hooks.extend(jwt_plugin.database_hooks);
68    }
69
70    for contribution in oauth_provider_schema() {
71        auth_plugin = auth_plugin.with_schema(contribution);
72    }
73    for endpoint in endpoints::oauth_provider_endpoints(Arc::clone(&shared)) {
74        auth_plugin = auth_plugin.with_endpoint(endpoint);
75    }
76    for rule in rate_limit_rules() {
77        auth_plugin = auth_plugin.with_rate_limit(rule);
78    }
79
80    Ok(OAuthProviderPlugin {
81        id: "oauth-provider".to_owned(),
82        version: VERSION.to_owned(),
83        options: resolved,
84        auth_plugin,
85    })
86}
87
88fn resolve_options(
89    options: OAuthProviderOptions,
90) -> Result<ResolvedOAuthProviderOptions, OAuthProviderConfigError> {
91    if options.login_page.is_empty() {
92        return Err(OAuthProviderConfigError::MissingLoginPage);
93    }
94    if options.consent_page.is_empty() {
95        return Err(OAuthProviderConfigError::MissingConsentPage);
96    }
97
98    let scopes = non_empty_or_default(
99        options.scopes,
100        ["openid", "profile", "email", "offline_access"],
101    );
102    let scope_set: HashSet<&str> = scopes.iter().map(String::as_str).collect();
103
104    let client_registration_allowed_scopes = merge_allowed_scopes(
105        options.client_registration_allowed_scopes,
106        &options.client_registration_default_scopes,
107    );
108    for scope in &client_registration_allowed_scopes {
109        if !scope_set.contains(scope.as_str()) {
110            return Err(OAuthProviderConfigError::UnknownClientRegistrationScope(
111                scope.clone(),
112            ));
113        }
114    }
115    for scope in &options.advertised_scopes_supported {
116        if !scope_set.contains(scope.as_str()) {
117            return Err(OAuthProviderConfigError::UnknownAdvertisedScope(
118                scope.clone(),
119            ));
120        }
121    }
122    if options
123        .pairwise_secret
124        .as_ref()
125        .is_some_and(|secret| secret.len() < 32)
126    {
127        return Err(OAuthProviderConfigError::PairwiseSecretTooShort);
128    }
129
130    let grant_types = if options.grant_types.is_empty() {
131        vec![
132            GrantType::AuthorizationCode,
133            GrantType::ClientCredentials,
134            GrantType::RefreshToken,
135        ]
136    } else {
137        options.grant_types
138    };
139    if grant_types.contains(&GrantType::RefreshToken)
140        && !grant_types.contains(&GrantType::AuthorizationCode)
141    {
142        return Err(OAuthProviderConfigError::RefreshTokenRequiresAuthorizationCode);
143    }
144
145    let store_client_secret =
146        resolve_client_secret_storage(options.store_client_secret, options.disable_jwt_plugin)?;
147    Ok(ResolvedOAuthProviderOptions {
148        claims: claims_for_scopes(&scope_set),
149        scopes,
150        client_registration_allowed_scopes,
151        grant_types,
152        login_page: options.login_page,
153        consent_page: options.consent_page,
154        code_expires_in: options.code_expires_in,
155        access_token_expires_in: options.access_token_expires_in,
156        m2m_access_token_expires_in: options.m2m_access_token_expires_in,
157        id_token_expires_in: options.id_token_expires_in,
158        refresh_token_expires_in: options.refresh_token_expires_in,
159        allow_unauthenticated_client_registration: options
160            .allow_unauthenticated_client_registration,
161        allow_dynamic_client_registration: options.allow_dynamic_client_registration,
162        disable_jwt_plugin: options.disable_jwt_plugin,
163        store_client_secret,
164        store_tokens: options.store_tokens,
165        pairwise_secret: options.pairwise_secret,
166        advertised_scopes_supported: options.advertised_scopes_supported,
167        valid_audiences: options.valid_audiences,
168    })
169}
170
171fn non_empty_or_default<const N: usize>(values: Vec<String>, default: [&str; N]) -> Vec<String> {
172    let values: Vec<String> = values
173        .into_iter()
174        .filter(|value| !value.is_empty())
175        .collect();
176    if values.is_empty() {
177        default.into_iter().map(str::to_owned).collect()
178    } else {
179        values
180    }
181}
182
183fn merge_allowed_scopes(mut allowed: Vec<String>, default_scopes: &[String]) -> Vec<String> {
184    if default_scopes.is_empty() {
185        return allowed;
186    }
187    for scope in default_scopes {
188        if !allowed.contains(scope) {
189            allowed.push(scope.clone());
190        }
191    }
192    allowed
193}
194
195fn claims_for_scopes(scopes: &HashSet<&str>) -> Vec<String> {
196    let mut claims = vec![
197        "sub".to_owned(),
198        "iss".to_owned(),
199        "aud".to_owned(),
200        "exp".to_owned(),
201        "iat".to_owned(),
202        "sid".to_owned(),
203        "scope".to_owned(),
204        "azp".to_owned(),
205    ];
206    if scopes.contains("email") {
207        claims.push("email".to_owned());
208        claims.push("email_verified".to_owned());
209    }
210    if scopes.contains("profile") {
211        claims.push("name".to_owned());
212        claims.push("picture".to_owned());
213        claims.push("family_name".to_owned());
214        claims.push("given_name".to_owned());
215    }
216    claims
217}
218
219fn resolve_client_secret_storage(
220    storage: SecretStorage,
221    disable_jwt_plugin: bool,
222) -> Result<SecretStorage, OAuthProviderConfigError> {
223    match (storage, disable_jwt_plugin) {
224        (SecretStorage::Auto, true) => Ok(SecretStorage::Encrypted),
225        (SecretStorage::Auto, false) => Ok(SecretStorage::Hashed),
226        (SecretStorage::Hashed, true) => {
227            Err(OAuthProviderConfigError::HashedClientSecretsRequireJwtPlugin)
228        }
229        (SecretStorage::Encrypted, false) => {
230            Err(OAuthProviderConfigError::EncryptedClientSecretsWithJwtPlugin)
231        }
232        (storage, _) => Ok(storage),
233    }
234}
235
236fn rate_limit_rules() -> Vec<PluginRateLimitRule> {
237    [
238        ("/oauth2/token", 60, 20),
239        ("/oauth2/authorize", 60, 30),
240        ("/oauth2/introspect", 60, 100),
241        ("/oauth2/revoke", 60, 30),
242        ("/oauth2/register", 60, 5),
243        ("/oauth2/userinfo", 60, 60),
244    ]
245    .into_iter()
246    .map(|(path, window, max)| PluginRateLimitRule::new(path, RateLimitRule { window, max }))
247    .collect()
248}