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 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
50pub const VERSION: &str = env!("CARGO_PKG_VERSION");
52
53pub 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}