1use 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#[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 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#[async_trait]
63pub trait OAuthExtensionGrantHandler: Send + Sync + std::fmt::Debug {
64 async fn handle(&self, input: OAuthExtensionGrantInput<'_>) -> Result<Value, EndpointError>;
65}
66#[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 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 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}