1mod 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 error::OAuthProviderError;
22pub use options::{
23 ClientPrivilegeAction, ClientPrivilegesInput, ClientPrivilegesResolver, ClientReferenceInput,
24 ClientReferenceResolver, ClientSecretHashInput, ClientSecretHashResolver,
25 ClientSecretVerifyInput, ClientSecretVerifyResolver, CustomAccessTokenClaimsInput,
26 CustomAccessTokenClaimsResolver, CustomIdTokenClaimsInput, CustomIdTokenClaimsResolver,
27 CustomTokenResponseFieldsInput, CustomTokenResponseFieldsResolver, CustomUserInfoClaimsInput,
28 CustomUserInfoClaimsResolver, GrantType, McpMetadataOverrides, McpOptions,
29 OAuthProviderConfigError, OAuthProviderOptions, OAuthProviderRateLimit,
30 OAuthProviderRateLimits, OAuthTokenPrefixes, PromptRedirectInput, PromptRedirectResolver,
31 PromptShouldRedirectResolver, RefreshTokenFormatDecodeOutput, RefreshTokenFormatEncodeInput,
32 RefreshTokenFormatter, RequestUriResolver, RequestUriResolverInput, ResolvedMcpOptions,
33 ResolvedOAuthProviderOptions, SecretStorage, StringGeneratorResolver, TokenEndpointAuthMethod,
34 TokenHashInput, TokenHashResolver, TrustedClientCache,
35};
36pub use rustauth_plugins::jwt::JwtOptions;
37
38#[cfg(feature = "test-util")]
39pub use authorize::{decide_authorize, AuthorizeDecision};
40#[cfg(feature = "test-util")]
41pub use client::{
42 check_oauth_client, oauth_to_schema, schema_to_oauth, CreateOAuthClientInput, OAuthClient,
43};
44#[cfg(feature = "test-util")]
45pub use consent::{
46 delete_consent, find_consent, has_granted_scopes, upsert_consent, ConsentGrantInput,
47};
48#[cfg(feature = "test-util")]
49pub use metadata::{
50 auth_server_metadata, oauth_authorization_server_metadata, oidc_server_metadata,
51 well_known_metadata_response, WELL_KNOWN_METADATA_CACHE_CONTROL,
52};
53#[cfg(feature = "test-util")]
54pub use models::{OAuthAccessToken, OAuthConsent, OAuthRefreshToken, SchemaClient};
55#[cfg(feature = "test-util")]
56pub use schema::{
57 oauth_provider_schema, OAUTH_ACCESS_TOKEN_MODEL, OAUTH_CLIENT_MODEL, OAUTH_CONSENT_MODEL,
58 OAUTH_REFRESH_TOKEN_MODEL,
59};
60#[cfg(feature = "test-util")]
61pub use token::{
62 create_client_credentials_token, decode_refresh_token, store_client_secret, store_token,
63 verify_client_secret, TokenResponse,
64};
65
66#[cfg(feature = "mcp-client")]
67pub use mcp::client::{McpAuthClient, McpAuthClientOptions, McpSession};
68
69use std::collections::HashSet;
70use std::sync::Arc;
71
72use rustauth_core::error::RustAuthError;
73use rustauth_core::options::RateLimitRule;
74use rustauth_core::plugin::{AuthPlugin, PluginInitOutput, PluginRateLimitRule};
75
76pub const VERSION: &str = env!("CARGO_PKG_VERSION");
78
79pub fn oauth_provider(
81 options: OAuthProviderOptions,
82) -> Result<AuthPlugin, OAuthProviderConfigError> {
83 let resolved = resolve_options(options)?;
84 let mut auth_plugin = AuthPlugin::new("oauth-provider").with_version(VERSION);
85 let shared = Arc::new(resolved);
86 let shared_for_init = Arc::clone(&shared);
87
88 auth_plugin = auth_plugin.with_init(move |context| {
89 if shared_for_init.disable_jwt_plugin {
90 return Ok(PluginInitOutput::default());
91 }
92 if !context.has_plugin("jwt") {
93 return Err(RustAuthError::InvalidConfig(
94 "oauth-provider requires the jwt plugin when disable_jwt_plugin is false; \
95 register jwt(...) in plugins before oauth_provider(...)"
96 .to_owned(),
97 ));
98 }
99 Ok(PluginInitOutput::default())
100 });
101
102 for contribution in schema::oauth_provider_schema() {
103 auth_plugin = auth_plugin.with_schema(contribution);
104 }
105 for endpoint in endpoints::oauth_provider_endpoints(Arc::clone(&shared)) {
106 auth_plugin = auth_plugin.with_endpoint(endpoint);
107 }
108 for rule in rate_limit_rules(&shared.rate_limits) {
109 auth_plugin = auth_plugin.with_rate_limit(rule);
110 }
111
112 Ok(auth_plugin)
113}
114
115pub fn apply_jwt_metadata_defaults(resolved: &mut ResolvedOAuthProviderOptions, jwt: &JwtOptions) {
117 if resolved.advertised_jwks_uri.is_none() {
118 resolved.advertised_jwks_uri = jwt.jwks.remote_url.clone();
119 }
120 if resolved.advertised_id_token_signing_algorithms.is_empty() {
121 resolved.advertised_id_token_signing_algorithms.push(
122 jwt.jwks
123 .key_pair_algorithm
124 .unwrap_or(rustauth_plugins::jwt::JwkAlgorithm::EdDsa)
125 .as_str()
126 .to_owned(),
127 );
128 }
129 if resolved.jwks_path == "/jwks" && jwt.jwks.jwks_path != "/jwks" {
130 resolved.jwks_path = jwt.jwks.jwks_path.clone();
131 }
132}
133
134pub(crate) fn resolve_options(
135 options: OAuthProviderOptions,
136) -> Result<ResolvedOAuthProviderOptions, OAuthProviderConfigError> {
137 if options.login_page.is_empty() {
138 return Err(OAuthProviderConfigError::MissingLoginPage);
139 }
140 if options.consent_page.is_empty() {
141 return Err(OAuthProviderConfigError::MissingConsentPage);
142 }
143
144 let scopes = non_empty_or_default(
145 options.scopes,
146 ["openid", "profile", "email", "offline_access"],
147 );
148 let scope_set: HashSet<&str> = scopes.iter().map(String::as_str).collect();
149
150 let client_registration_default_scopes = options.client_registration_default_scopes;
151 let client_registration_allowed_scopes = merge_allowed_scopes(
152 options.client_registration_allowed_scopes,
153 &client_registration_default_scopes,
154 );
155 for scope in &client_registration_allowed_scopes {
156 if !scope_set.contains(scope.as_str()) {
157 return Err(OAuthProviderConfigError::UnknownClientRegistrationScope(
158 scope.clone(),
159 ));
160 }
161 }
162 for scope in &options.advertised_scopes_supported {
163 if !scope_set.contains(scope.as_str()) {
164 return Err(OAuthProviderConfigError::UnknownAdvertisedScope(
165 scope.clone(),
166 ));
167 }
168 }
169 for scope in &options.client_credential_grant_default_scopes {
170 if !scope_set.contains(scope.as_str()) {
171 return Err(OAuthProviderConfigError::UnknownClientCredentialGrantScope(
172 scope.clone(),
173 ));
174 }
175 }
176 if options
177 .pairwise_secret
178 .as_ref()
179 .is_some_and(|secret| secret.len() < 32)
180 {
181 return Err(OAuthProviderConfigError::PairwiseSecretTooShort);
182 }
183
184 let grant_types = if options.grant_types.is_empty() {
185 vec![
186 GrantType::AuthorizationCode,
187 GrantType::ClientCredentials,
188 GrantType::RefreshToken,
189 ]
190 } else {
191 options.grant_types
192 };
193 if grant_types.contains(&GrantType::RefreshToken)
194 && !grant_types.contains(&GrantType::AuthorizationCode)
195 {
196 return Err(OAuthProviderConfigError::RefreshTokenRequiresAuthorizationCode);
197 }
198
199 let mcp = resolve_mcp_options(options.mcp)?;
200 let store_client_secret =
201 resolve_client_secret_storage(options.store_client_secret, options.disable_jwt_plugin)?;
202 Ok(ResolvedOAuthProviderOptions {
203 claims: claims_for_scopes(&scope_set),
204 scopes,
205 client_registration_allowed_scopes,
206 grant_types,
207 login_page: options.login_page,
208 consent_page: options.consent_page,
209 signup_page: options.signup_page,
210 select_account_page: options.select_account_page,
211 post_login_page: options.post_login_page,
212 signup_redirect: options.signup_redirect,
213 select_account_redirect: options.select_account_redirect,
214 post_login_redirect: options.post_login_redirect,
215 signup_should_redirect: options.signup_should_redirect,
216 select_account_should_redirect: options.select_account_should_redirect,
217 post_login_should_redirect: options.post_login_should_redirect,
218 consent_reference_id: options.consent_reference_id,
219 code_expires_in: options.code_expires_in,
220 access_token_expires_in: options.access_token_expires_in,
221 m2m_access_token_expires_in: options.m2m_access_token_expires_in,
222 id_token_expires_in: options.id_token_expires_in,
223 refresh_token_expires_in: options.refresh_token_expires_in,
224 client_credential_grant_default_scopes: options.client_credential_grant_default_scopes,
225 scope_expirations: options.scope_expirations,
226 client_registration_default_scopes,
227 client_registration_client_secret_expiration: options
228 .client_registration_client_secret_expiration,
229 allow_unauthenticated_client_registration: options
230 .allow_unauthenticated_client_registration,
231 allow_dynamic_client_registration: options.allow_dynamic_client_registration,
232 allow_public_client_prelogin: options.allow_public_client_prelogin,
233 cached_trusted_clients: options.cached_trusted_clients,
234 trusted_client_cache: TrustedClientCache::default(),
235 client_reference: options.client_reference,
236 client_privileges: options.client_privileges,
237 custom_access_token_claims: options.custom_access_token_claims,
238 custom_id_token_claims: options.custom_id_token_claims,
239 custom_token_response_fields: options.custom_token_response_fields,
240 custom_userinfo_claims: options.custom_userinfo_claims,
241 request_uri_resolver: options.request_uri_resolver,
242 prefixes: options.prefixes,
243 generate_client_id: options.generate_client_id,
244 generate_client_secret: options.generate_client_secret,
245 generate_opaque_access_token: options.generate_opaque_access_token,
246 generate_refresh_token: options.generate_refresh_token,
247 format_refresh_token: options.format_refresh_token,
248 disable_jwt_plugin: options.disable_jwt_plugin,
249 store_client_secret,
250 store_tokens: options.store_tokens,
251 hash_client_secret: options.hash_client_secret,
252 verify_client_secret_hash: options.verify_client_secret_hash,
253 hash_token: options.hash_token,
254 pairwise_secret: options.pairwise_secret,
255 advertised_scopes_supported: options.advertised_scopes_supported,
256 advertised_claims_supported: options.advertised_claims_supported,
257 advertised_jwks_uri: options.advertised_jwks_uri,
258 advertised_id_token_signing_algorithms: options.advertised_id_token_signing_algorithms,
259 jwks_path: options.jwks_path,
260 valid_audiences: options.valid_audiences,
261 rate_limits: options.rate_limits,
262 mcp,
263 })
264}
265
266pub fn resolve_oauth_provider_options(
268 options: OAuthProviderOptions,
269) -> Result<ResolvedOAuthProviderOptions, OAuthProviderConfigError> {
270 resolve_options(options)
271}
272
273fn resolve_mcp_options(
274 options: Option<McpOptions>,
275) -> Result<Option<ResolvedMcpOptions>, OAuthProviderConfigError> {
276 options
277 .map(|options| {
278 if let Some(resource) = &options.resource {
279 let parsed = url::Url::parse(resource)
280 .map_err(|_| OAuthProviderConfigError::InvalidMcpResource)?;
281 if !parsed.has_host() {
282 return Err(OAuthProviderConfigError::InvalidMcpResource);
283 }
284 }
285 Ok(ResolvedMcpOptions {
286 resource: options.resource,
287 metadata: options.metadata,
288 })
289 })
290 .transpose()
291}
292
293fn non_empty_or_default<const N: usize>(values: Vec<String>, default: [&str; N]) -> Vec<String> {
294 let values: Vec<String> = values
295 .into_iter()
296 .filter(|value| !value.is_empty())
297 .collect();
298 if values.is_empty() {
299 default.into_iter().map(str::to_owned).collect()
300 } else {
301 values
302 }
303}
304
305fn merge_allowed_scopes(mut allowed: Vec<String>, default_scopes: &[String]) -> Vec<String> {
306 if default_scopes.is_empty() {
307 return allowed;
308 }
309 for scope in default_scopes {
310 if !allowed.contains(scope) {
311 allowed.push(scope.clone());
312 }
313 }
314 allowed
315}
316
317fn claims_for_scopes(scopes: &HashSet<&str>) -> Vec<String> {
318 let mut claims = vec![
319 "sub".to_owned(),
320 "iss".to_owned(),
321 "aud".to_owned(),
322 "exp".to_owned(),
323 "iat".to_owned(),
324 "sid".to_owned(),
325 "scope".to_owned(),
326 "azp".to_owned(),
327 ];
328 if scopes.contains("email") {
329 claims.push("email".to_owned());
330 claims.push("email_verified".to_owned());
331 }
332 if scopes.contains("profile") {
333 claims.push("name".to_owned());
334 claims.push("picture".to_owned());
335 claims.push("family_name".to_owned());
336 claims.push("given_name".to_owned());
337 }
338 claims
339}
340
341fn resolve_client_secret_storage(
342 storage: SecretStorage,
343 disable_jwt_plugin: bool,
344) -> Result<SecretStorage, OAuthProviderConfigError> {
345 match (storage, disable_jwt_plugin) {
346 (SecretStorage::Auto, true) => Ok(SecretStorage::Encrypted),
347 (SecretStorage::Auto, false) => Ok(SecretStorage::Hashed),
348 (SecretStorage::Hashed, true) => {
349 Err(OAuthProviderConfigError::HashedClientSecretsRequireJwtPlugin)
350 }
351 (SecretStorage::Encrypted, false) => {
352 Err(OAuthProviderConfigError::EncryptedClientSecretsWithJwtPlugin)
353 }
354 (storage, _) => Ok(storage),
355 }
356}
357
358fn rate_limit_rules(options: &OAuthProviderRateLimits) -> Vec<PluginRateLimitRule> {
359 [
360 (
361 "/oauth2/token",
362 RateLimitRule::new(time::Duration::seconds(60), 20),
363 &options.token,
364 ),
365 (
366 "/oauth2/authorize",
367 RateLimitRule::new(time::Duration::seconds(60), 30),
368 &options.authorize,
369 ),
370 (
371 "/oauth2/introspect",
372 RateLimitRule::new(time::Duration::seconds(60), 100),
373 &options.introspect,
374 ),
375 (
376 "/oauth2/revoke",
377 RateLimitRule::new(time::Duration::seconds(60), 30),
378 &options.revoke,
379 ),
380 (
381 "/oauth2/register",
382 RateLimitRule::new(time::Duration::seconds(60), 5),
383 &options.register,
384 ),
385 (
386 "/oauth2/userinfo",
387 RateLimitRule::new(time::Duration::seconds(60), 60),
388 &options.userinfo,
389 ),
390 ]
391 .into_iter()
392 .filter_map(|(path, default, setting)| match setting {
393 OAuthProviderRateLimit::Default => Some(PluginRateLimitRule::new(path, default)),
394 OAuthProviderRateLimit::Disabled => None,
395 OAuthProviderRateLimit::Custom(rule) => Some(PluginRateLimitRule::new(path, rule.clone())),
396 })
397 .collect()
398}