Skip to main content

alibi_plugins/oauth_provider/
extensions.rs

1//! Configured extension grants and schema-erased, grant-bound provider capabilities.
2use super::{
3    OAuthAuthenticatedClient, OAuthClientAuthenticationOptions, OAuthProviderConfig,
4    OAuthProviderPlugin, OAuthTokenIssueParams,
5};
6use alibi_core::endpoint::{
7    EndpointCall, EndpointContextPatch, EndpointDefinition, EndpointError, EndpointOptions,
8    EndpointResponse, with_endpoint_call_context,
9};
10use alibi_core::session::SessionRequest;
11use alibi_core::store::OAuthClient;
12use alibi_core::utils::json::JsValue;
13use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, CallbackContext, HttpMethod};
14use async_trait::async_trait;
15use serde_json::Value;
16use std::sync::Arc;
17
18/// One configured extension. Dispatched URI keys must be disjoint across extensions.
19#[derive(Clone, Debug, Default)]
20pub struct OAuthProviderExtension {
21    pub grants: indexmap::IndexMap<String, Arc<dyn OAuthExtensionGrantHandler>>,
22    pub metadata: Option<Arc<dyn OAuthMetadataContributor>>,
23    pub claims: super::OAuthExtensionClaims,
24    pub client_authentication:
25        indexmap::IndexMap<String, super::OAuthExtensionClientAuthentication>,
26}
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum OAuthMetadataType {
29    OAuthAuthorizationServer,
30    OpenIdConfiguration,
31}
32impl OAuthMetadataType {
33    #[must_use]
34    pub const fn as_str(self) -> &'static str {
35        match self {
36            Self::OAuthAuthorizationServer => "oauth-authorization-server",
37            Self::OpenIdConfiguration => "openid-configuration",
38        }
39    }
40}
41pub struct OAuthMetadataExtensionInput<'a> {
42    pub context: &'a CallbackContext,
43    pub options: &'a OAuthProviderConfig,
44    pub metadata_type: OAuthMetadataType,
45    /// Provider and client-discovery document, before any metadata callback contribution.
46    pub document: &'a mut Value,
47}
48pub trait OAuthMetadataContributor: Send + Sync + std::fmt::Debug {
49    fn contribute(
50        &self,
51        input: &mut OAuthMetadataExtensionInput<'_>,
52    ) -> AuthResult<serde_json::Map<String, Value>>;
53}
54pub struct OAuthExtensionGrantInput<'a> {
55    pub context: CallbackContext,
56    pub call: &'a EndpointCall,
57    pub options: &'a OAuthProviderConfig,
58    pub grant_type: &'a str,
59    pub provider: &'a dyn OAuthProviderApi,
60}
61/// A trusted companion owns grant-specific admission; no client is fabricated or pre-authorized.
62#[async_trait]
63pub trait OAuthExtensionGrantHandler: Send + Sync + std::fmt::Debug {
64    async fn handle(&self, input: OAuthExtensionGrantInput<'_>) -> Result<Value, EndpointError>;
65}
66/// Live provider capabilities bound to the actual call and its optional grant.
67#[async_trait]
68pub trait OAuthProviderApi: Send + Sync {
69    async fn get_client(&self, client_id: &str) -> AuthResult<Option<OAuthClient>>;
70    async fn authenticate_client(
71        &self,
72        options: &OAuthClientAuthenticationOptions,
73    ) -> Result<OAuthAuthenticatedClient, EndpointError>;
74    async fn hash_token(&self, token: &str, kind: &str) -> AuthResult<String>;
75    async fn issue_tokens(&self, params: &OAuthTokenIssueParams) -> Result<Value, EndpointError>;
76    async fn validate_access_token(
77        &self,
78        token: &str,
79        client_id: Option<&str>,
80    ) -> Result<Value, EndpointError>;
81    async fn require_active_access_token(
82        &self,
83        token: &str,
84        client_id: Option<&str>,
85    ) -> Result<Value, EndpointError>;
86}
87struct ProviderApi<'a, S: AuthSchema> {
88    provider: &'a OAuthProviderPlugin,
89    context: &'a AuthContext<S>,
90    call: &'a EndpointCall,
91    grant_type: Option<&'a str>,
92}
93#[async_trait]
94impl<S: AuthSchema> OAuthProviderApi for ProviderApi<'_, S> {
95    async fn get_client(&self, id: &str) -> AuthResult<Option<OAuthClient>> {
96        self.provider.get_client(self.context, id).await
97    }
98    async fn authenticate_client(
99        &self,
100        options: &OAuthClientAuthenticationOptions,
101    ) -> Result<OAuthAuthenticatedClient, EndpointError> {
102        self.provider
103            .authenticate_client(self.context, self.call, self.grant_type, options)
104            .await
105    }
106    async fn hash_token(&self, token: &str, kind: &str) -> AuthResult<String> {
107        self.provider.hash_token(token, kind).await
108    }
109    async fn issue_tokens(&self, params: &OAuthTokenIssueParams) -> Result<Value, EndpointError> {
110        self.provider
111            .issue_tokens(self.context, self.call, self.grant_type, params)
112            .await
113    }
114    async fn validate_access_token(
115        &self,
116        token: &str,
117        id: Option<&str>,
118    ) -> Result<Value, EndpointError> {
119        self.provider
120            .validate_access_token(self.context, token, id)
121            .await
122    }
123    async fn require_active_access_token(
124        &self,
125        token: &str,
126        id: Option<&str>,
127    ) -> Result<Value, EndpointError> {
128        self.provider
129            .require_active_access_token(self.context, token, id)
130            .await
131    }
132}
133impl OAuthProviderPlugin {
134    pub(super) fn apply_metadata_extensions<S: AuthSchema>(
135        &self,
136        context: &AuthContext<S>,
137        mut document: Value,
138    ) -> AuthResult<Value> {
139        let mut next = document
140            .as_object()
141            .cloned()
142            .ok_or_else(|| AuthError::internal("OAuth metadata must be an object"))?;
143        let callback = CallbackContext::new(context, None);
144        let metadata_type = if self.config.scopes.iter().any(|scope| scope == "openid") {
145            OAuthMetadataType::OpenIdConfiguration
146        } else {
147            OAuthMetadataType::OAuthAuthorizationServer
148        };
149        for extension in &self.config.extensions {
150            if let Some(contributor) = &extension.metadata {
151                let mut input = OAuthMetadataExtensionInput {
152                    context: &callback,
153                    options: &self.config,
154                    metadata_type,
155                    document: &mut document,
156                };
157                for (key, value) in contributor.contribute(&mut input)? {
158                    // Reference `key in next` also protects inherited Object.prototype keys.
159                    if !next.contains_key(&key)
160                        && !matches!(
161                            key.as_str(),
162                            "__proto__"
163                                | "constructor"
164                                | "toString"
165                                | "toLocaleString"
166                                | "valueOf"
167                                | "hasOwnProperty"
168                                | "isPrototypeOf"
169                                | "propertyIsEnumerable"
170                                | "__defineGetter__"
171                                | "__defineSetter__"
172                                | "__lookupGetter__"
173                                | "__lookupSetter__"
174                        )
175                    {
176                        _ = next.insert(key, value);
177                    }
178                }
179            }
180        }
181        Ok(Value::Object(next))
182    }
183    /// Obtain live capabilities for an extension or a trusted companion endpoint.
184    /// Issuance without a bound grant is rejected by the shared issuance owner.
185    pub fn get_oauth_provider_api<'a, S: AuthSchema>(
186        &'a self,
187        context: &'a AuthContext<S>,
188        call: &'a EndpointCall,
189        grant_type: Option<&'a str>,
190    ) -> impl OAuthProviderApi + 'a {
191        ProviderApi {
192            provider: self,
193            context,
194            call,
195            grant_type,
196        }
197    }
198    pub(super) fn supported_grant_types(&self) -> Vec<String> {
199        super::unique(
200            self.config
201                .grant_types
202                .iter()
203                .cloned()
204                .chain(
205                    self.config
206                        .extensions
207                        .iter()
208                        .flat_map(|extension| extension.grants.keys().cloned()),
209                )
210                .collect(),
211        )
212    }
213    pub(super) fn validate_extensions(&self) -> AuthResult<()> {
214        let mut grants = Vec::new();
215        let mut auth_methods = Vec::new();
216        let mut assertion_types = Vec::new();
217        for extension in &self.config.extensions {
218            for grant in extension.grants.keys() {
219                if alibi_core::utils::javascript::trim(grant).is_empty() {
220                    return Err(AuthError::config(
221                        "OAuth Provider extension grant type cannot be empty",
222                    ));
223                }
224                if url::Url::parse(grant).is_err() {
225                    return Err(AuthError::config(format!(
226                        "OAuth Provider extension grant type must be an absolute URI: {grant}"
227                    )));
228                }
229                grants.push(grant);
230            }
231            for (method, strategy) in &extension.client_authentication {
232                if alibi_core::utils::javascript::trim(method).is_empty() {
233                    return Err(AuthError::config(
234                        "OAuth Provider extension token_endpoint_auth_method cannot be empty",
235                    ));
236                }
237                if [
238                    "none",
239                    "client_secret_basic",
240                    "client_secret_post",
241                    "private_key_jwt",
242                ]
243                .contains(&method.as_str())
244                {
245                    return Err(AuthError::config(format!(
246                        "OAuth Provider extension token_endpoint_auth_method is reserved: {method}"
247                    )));
248                }
249                auth_methods.push(method);
250                let types = strategy
251                    .assertion_types
252                    .as_deref()
253                    .unwrap_or_else(|| std::slice::from_ref(method));
254                if types.is_empty() {
255                    return Err(AuthError::config(format!(
256                        "OAuth Provider extension client_assertion_type list cannot be empty for {method}"
257                    )));
258                }
259                for assertion_type in types {
260                    if alibi_core::utils::javascript::trim(assertion_type).is_empty() {
261                        return Err(AuthError::config(
262                            "OAuth Provider extension client_assertion_type cannot be empty",
263                        ));
264                    }
265                    if url::Url::parse(assertion_type).is_err() {
266                        return Err(AuthError::config(format!(
267                            "OAuth Provider extension client_assertion_type must be an absolute URI: {assertion_type}"
268                        )));
269                    }
270                    if assertion_type == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
271                        return Err(AuthError::config(format!(
272                            "OAuth Provider extension client_assertion_type is reserved: {assertion_type}"
273                        )));
274                    }
275                    assertion_types.push(assertion_type);
276                }
277            }
278        }
279        for (label, values) in [
280            ("grant type", grants),
281            ("token_endpoint_auth_method", auth_methods),
282            ("client_assertion_type", assertion_types),
283        ] {
284            let mut seen = std::collections::HashSet::new();
285            for value in values {
286                if !seen.insert(value) {
287                    return Err(AuthError::config(format!(
288                        "OAuth Provider extensions register {label} \"{value}\" more than once. Extension contributions must be disjoint."
289                    )));
290                }
291            }
292        }
293        Ok(())
294    }
295    pub(super) async fn extension_grant<S: AuthSchema>(
296        &self,
297        grant: &str,
298        input: Option<&JsValue>,
299        request: &impl SessionRequest,
300        context: &AuthContext<S>,
301    ) -> AuthResult<Option<EndpointResponse>> {
302        let Some(handler) = self
303            .config
304            .extensions
305            .iter()
306            .find_map(|extension| extension.grants.get(grant))
307            .cloned()
308        else {
309            return Ok(None);
310        };
311        let mut call = request.endpoint_call().cloned().unwrap_or_else(|| {
312            EndpointCall::new(
313                &EndpointDefinition {
314                    name: "oauth2Token",
315                    operation_id: "oauth2Token",
316                    path: Some("/oauth2/token".into()),
317                    method: HttpMethod::Post,
318                },
319                input.cloned(),
320                None,
321                EndpointOptions {
322                    method: Some(request.session_method().clone()),
323                    headers: Some(request.session_headers().clone()),
324                    request: request.physical_request().cloned(),
325                },
326            )
327        });
328        EndpointContextPatch {
329            body: Some(JsValue::Object(
330                [("grant_type".into(), JsValue::String(grant.into()))]
331                    .into_iter()
332                    .collect(),
333            )),
334            ..Default::default()
335        }
336        .apply(&mut call);
337        let api = self.get_oauth_provider_api(context, &call, Some(grant));
338        let result = with_endpoint_call_context(call.clone(), async {
339            handler
340                .handle(OAuthExtensionGrantInput {
341                    context: CallbackContext::new(context, request.physical_request()),
342                    call: &call,
343                    options: &self.config,
344                    grant_type: grant,
345                    provider: &api,
346                })
347                .await
348        })
349        .await;
350        let response = match result {
351            Ok(value) => EndpointResponse::json(&value)?,
352            Err(error) => {
353                let mut response = EndpointResponse::error(error.error);
354                if let Some(body) = error.body {
355                    response = response.with_error_body(*body);
356                }
357                if error.body_absent {
358                    response = response.without_error_body();
359                }
360                if let Some(headers) = error.headers {
361                    response.merge_headers(headers);
362                }
363                response
364            }
365        };
366        Ok(Some(
367            response
368                .with_header("Cache-Control", "no-store")
369                .with_header("Pragma", "no-cache"),
370        ))
371    }
372}