1use super::OAuthProviderPlugin;
2use alibi_core::endpoint::{EndpointResponse, ServerEndpoint};
3use alibi_core::session::SessionRequest;
4use alibi_core::store::CreateOAuthClient;
5use alibi_core::store::oauth_clients::{
6 OAuthClientRegistrationWriteError, PersistOAuthClientRegistration,
7};
8use alibi_core::utils::javascript::trim as js_trim;
9use alibi_core::utils::json::JsValue;
10use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, AuthUser};
11use rand::{RngExt as _, distr::Alphanumeric};
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct CreateOAuthClientInput {
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub redirect_uris: Option<Vec<String>>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub scope: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub client_name: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub client_uri: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub logo_uri: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub contacts: Option<Vec<String>>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub tos_uri: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub policy_uri: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub software_id: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub software_version: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub software_statement: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub post_logout_redirect_uris: Option<Vec<String>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub backchannel_logout_uri: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub backchannel_logout_session_required: Option<bool>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub token_endpoint_auth_method: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub application_type: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub jwks: Option<Value>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub jwks_uri: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub grant_types: Option<Vec<String>>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub response_types: Option<Vec<String>>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub dpop_bound_access_tokens: Option<bool>,
60}
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub struct RegisterOAuthClientInput {
65 #[serde(flatten)]
66 pub metadata: CreateOAuthClientInput,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub resources: Option<Vec<String>>,
69}
70impl OAuthProviderPlugin {
71 async fn require_client_privileges<S: AuthSchema>(
72 &self,
73 action: &'static str,
74 client_id: Option<&str>,
75 req: &impl SessionRequest,
76 ctx: &AuthContext<S>,
77 ) -> AuthResult<Option<EndpointResponse>> {
78 let Ok((user, session)) = Box::pin(ctx.require_cached_session(req)).await else {
79 return Ok(Some(client_privilege_denied(401)));
80 };
81 if req
82 .endpoint_call()
83 .is_some_and(|call| call.headers().is_none())
84 {
85 return Ok(Some(client_privilege_denied(400)));
86 }
87 let scope_policy = (action == "configure-client-credentials-scopes")
88 .then_some(self.config.client_credentials_scope_privileges.as_ref())
89 .flatten();
90 if action == "configure-client-credentials-scopes"
91 && self.config.client_privileges.is_none()
92 && scope_policy.is_none()
93 {
94 return Ok(Some(client_privilege_denied(401)));
95 }
96 if self.config.client_privileges.is_some() || scope_policy.is_some() {
97 let mut headers = alibi_core::Headers::new();
98 for (name, value) in req.session_headers() {
99 _ = headers.insert(name.clone(), value.clone());
100 }
101 let input = super::OAuthClientPrivilegeContext {
102 action,
103 headers,
104 user: serde_json::to_value(user)?,
105 session: serde_json::to_value(session)?,
106 client_id: client_id.map(str::to_owned),
107 };
108 for policy in self.config.client_privileges.iter().chain(scope_policy) {
109 if !policy.allowed(&input).await? {
110 return Ok(Some(client_privilege_denied(401)));
111 }
112 }
113 }
114 Ok(None)
115 }
116 pub fn create_oauth_client_endpoint(
117 input: &CreateOAuthClientInput,
118 ) -> AuthResult<ServerEndpoint<Value>> {
119 ServerEndpoint::new("oauth-provider", "createOAuthClient").with_body(input)
120 }
121 pub(super) async fn create_client<S: AuthSchema>(
122 &self,
123 input: CreateOAuthClientInput,
124 req: &impl SessionRequest,
125 ctx: &AuthContext<S>,
126 ) -> AuthResult<EndpointResponse> {
127 let (user, _) = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
128 if req
129 .endpoint_call()
130 .is_some_and(|call| call.headers().is_none())
131 {
132 return Ok(EndpointResponse::error(AuthError::Api {
133 status: 400,
134 code: None,
135 message: String::new(),
136 })
137 .without_error_body());
138 }
139 req.queue_response_header("Cache-Control", "no-store");
140 req.queue_response_header("Pragma", "no-cache");
141 if let Some(response) = self
142 .require_client_privileges("create", None, req, ctx)
143 .await?
144 {
145 return Ok(response);
146 }
147 let metadata: serde_json::Map<String, Value> =
148 serde_json::from_value(serde_json::to_value(input)?)?;
149 let metadata = match self.admit_metadata(metadata, &self.config.scopes, ctx)? {
150 Ok(metadata) => metadata,
151 Err(response) => return Ok(response),
152 };
153 self.persist_client(metadata, Some(user.id().as_ref().to_owned()), false, ctx)
154 .await
155 }
156}
157fn random_credential() -> String {
158 rand::rng()
159 .sample_iter(Alphanumeric)
160 .map(char::from)
161 .filter(char::is_ascii_alphabetic)
162 .take(32)
163 .collect()
164}
165fn protocol_error(error: &str, description: impl Into<String>) -> AuthResult<EndpointResponse> {
166 Ok(EndpointResponse::error(AuthError::Api {
167 status: 400,
168 code: None,
169 message: String::new(),
170 })
171 .with_error_body(alibi_core::utils::json::parse_value(
172 &serde_json::to_string(&json!({"error":error,"error_description":description.into()}))?,
173 )?))
174}
175fn redirect_error(uri: &str, application: &str) -> Option<String> {
176 let Ok(url) = url::Url::parse(uri) else {
177 return Some(format!("redirect URI must be an absolute URI: {uri}"));
178 };
179 if uri.contains('#')
180 || !url.username().is_empty()
181 || url.password().is_some_and(|value| !value.is_empty())
182 {
183 return Some(format!(
184 "redirect URI must not include credentials or a fragment: {uri}"
185 ));
186 }
187 let host = url.host_str().unwrap_or_default();
188 if host.trim_end_matches('.') == "localhost" && host.ends_with('.') {
189 return Some(format!(
190 "redirect URI localhost must not include trailing dots: {uri}"
191 ));
192 }
193 let loopback = host == "localhost"
194 || host
195 .parse::<std::net::IpAddr>()
196 .is_ok_and(|ip| ip.is_loopback())
197 || host == "[::1]";
198 if application == "web" {
199 return (url.scheme() != "https" || loopback).then(|| {
200 format!("web clients require https redirect URIs on non-loopback hosts: {uri}")
201 });
202 }
203 if url.scheme() == "https" {
204 return loopback
205 .then(|| format!("native clients must not use https loopback redirect URIs: {uri}"));
206 }
207 if url.scheme() == "http" {
208 let raw_host = raw_http_host(uri);
209 return (!raw_host.as_deref().is_some_and(|host|["localhost","127.0.0.1","[::1]"].contains(&host))).then(||format!("native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: {uri}"));
210 }
211 (!native_private_use(&url, uri)).then(|| format!("native private-use redirect URI schemes must not use a reserved scheme; they must be an authority-free reverse-domain URI or a custom-scheme URI with a host and a path: {uri}"))
212}
213pub(super) fn native_private_use(url: &url::Url, uri: &str) -> bool {
216 let scheme = url.scheme();
217 if ["http", "https"].contains(&scheme) {
218 return false;
219 }
220 if url.host_str().is_none_or(str::is_empty) {
221 let labels = scheme.split('.').collect::<Vec<_>>();
222 return labels.len() > 1
223 && labels.iter().all(|label| {
224 !label.is_empty()
225 && !label.starts_with('-')
226 && !label.ends_with('-')
227 && label
228 .bytes()
229 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
230 })
231 && uri
232 .get(scheme.len() + 1..)
233 .is_some_and(|path| path.starts_with('/') && !path.starts_with("//"));
234 }
235 ![
238 "ws",
239 "wss",
240 "file",
241 "ftp",
242 "mailto",
243 "javascript",
244 "data",
245 "vbscript",
246 "blob",
247 "about",
248 "view-source",
249 ]
250 .contains(&scheme)
251 && url.path().starts_with('/')
252}
253#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub enum OAuthClientRegistrationApplicationType {
257 #[default]
259 Web,
260 Native,
261 Infer,
264}
265impl OAuthClientRegistrationApplicationType {
266 fn resolve(self, redirects: Option<&Value>) -> &'static str {
267 match self {
268 Self::Web => "web",
269 Self::Native => "native",
270 Self::Infer => {
271 let native = redirects
272 .and_then(Value::as_array)
273 .into_iter()
274 .flatten()
275 .filter_map(Value::as_str)
276 .any(|uri| {
277 url::Url::parse(uri)
278 .is_ok_and(|url| !["http", "https"].contains(&url.scheme()))
279 || raw_http_host(uri).is_some_and(|host| {
280 ["localhost", "127.0.0.1", "[::1]"].contains(&host.as_str())
281 })
282 });
283 if native { "native" } else { "web" }
284 }
285 }
286 }
287}
288fn raw_http_host(uri: &str) -> Option<String> {
289 if !uri
290 .get(..7)
291 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
292 {
293 return None;
294 }
295 let authority = uri.get(7..)?.split(['/', '?', '#']).next()?;
296 let host = authority.rsplit('@').next()?;
297 if host.starts_with('[') {
298 host.find(']')
299 .and_then(|end| host.get(..=end))
300 .map(str::to_ascii_lowercase)
301 } else {
302 host.split(':').next().map(str::to_ascii_lowercase)
303 }
304}
305fn private_host(host: &str) -> bool {
306 !crate::sso::endpoint_host::public(host)
307}
308
309fn remote_key_uri_error<S: AuthSchema>(uri: &str, ctx: &AuthContext<S>) -> Option<String> {
310 let Ok(url) = url::Url::parse(uri) else {
311 return Some("jwks_uri must be a valid URL".into());
312 };
313 if url.scheme() != "https" {
314 return Some("jwks_uri must use HTTPS".into());
315 }
316 if !url.username().is_empty() || url.password().is_some_and(|value| !value.is_empty()) {
317 return Some("jwks_uri must not contain credentials".into());
318 }
319 if uri.contains('#') {
320 return Some("jwks_uri must not include a fragment component".into());
321 }
322 if private_host(url.host_str().unwrap_or_default()) {
323 return Some("jwks_uri must not point to a private or reserved address".into());
324 }
325 (!ctx.config.is_redirect_target_trusted(uri)).then(|| {
326 "jwks_uri must belong to a trusted origin or the Client ID Metadata Document origin".into()
327 })
328}
329fn backchannel_uri_error(uri: &str) -> Option<String> {
330 let Ok(url) = url::Url::parse(uri) else {
331 return Some("backchannel_logout_uri must be an absolute URL".into());
332 };
333 if uri.contains('#') {
334 return Some("backchannel_logout_uri must not include a fragment component".into());
335 }
336 if url.scheme() != "https" {
337 return Some("backchannel_logout_uri must use https".into());
338 }
339 if !url.username().is_empty() || url.password().is_some_and(|value| !value.is_empty()) {
340 return Some("backchannel_logout_uri must not contain credentials".into());
341 }
342 private_host(url.host_str().unwrap_or_default())
343 .then(|| "backchannel_logout_uri must not point to a private or reserved address".into())
344}
345pub(super) fn public_jwks_error(value: &Value) -> Option<&'static str> {
346 let keys = value.get("keys").and_then(Value::as_array)?;
347 for key in keys {
348 if ["k", "d", "p", "q", "dp", "dq", "qi", "oth"]
349 .iter()
350 .any(|name| key.get(name).is_some())
351 || key.get("kty").and_then(Value::as_str) == Some("oct")
352 {
353 return Some("jwks must contain only public asymmetric keys");
354 }
355 let string = |name| {
356 key.get(name)
357 .and_then(Value::as_str)
358 .is_some_and(|value| !value.is_empty())
359 };
360 let curve = key.get("crv").and_then(Value::as_str).unwrap_or_default();
361 let kind = key.get("kty").and_then(Value::as_str).unwrap_or_default();
362 let valid = match kind {
363 "RSA" => string("n") && string("e"),
364 "EC" => ["P-256", "P-384", "P-521"].contains(&curve) && string("x") && string("y"),
365 "OKP" => curve == "Ed25519" && string("x"),
366 _ => false,
367 };
368 if !valid {
369 return Some("jwks keys must be supported public JWKs with required key parameters");
370 }
371 if let Some(algorithm) = key.get("alg") {
372 let valid = algorithm.as_str().is_some_and(|alg| match kind {
373 "RSA" => ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"].contains(&alg),
374 "EC" => matches!(
375 (curve, alg),
376 ("P-256", "ES256") | ("P-384", "ES384") | ("P-521", "ES512")
377 ),
378 "OKP" => alg == "EdDSA",
379 _ => false,
380 });
381 if !valid {
382 return Some(
383 "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve",
384 );
385 }
386 }
387 }
388 None
389}
390#[derive(Clone, Copy)]
391enum FieldKind {
392 String,
393 Boolean,
394 Uris,
395 Uri,
396 Contacts,
397 Method,
398 Application,
399 Jwks,
400 Grants,
401 Responses,
402 Scopes,
403 Subject,
404 Expiration,
405 Record,
406 Resources,
407}
408const FIELDS: &[(&str, FieldKind)] = &[
409 ("redirect_uris", FieldKind::Uris),
410 ("scope", FieldKind::String),
411 ("client_name", FieldKind::String),
412 ("client_uri", FieldKind::String),
413 ("logo_uri", FieldKind::String),
414 ("contacts", FieldKind::Contacts),
415 ("tos_uri", FieldKind::String),
416 ("policy_uri", FieldKind::String),
417 ("software_id", FieldKind::String),
418 ("software_version", FieldKind::String),
419 ("software_statement", FieldKind::String),
420 ("post_logout_redirect_uris", FieldKind::Uris),
421 ("backchannel_logout_uri", FieldKind::Uri),
422 ("backchannel_logout_session_required", FieldKind::Boolean),
423 ("token_endpoint_auth_method", FieldKind::Method),
424 ("application_type", FieldKind::Application),
425 ("jwks", FieldKind::Jwks),
426 ("jwks_uri", FieldKind::String),
427 ("grant_types", FieldKind::Grants),
428 ("response_types", FieldKind::Responses),
429 ("dpop_bound_access_tokens", FieldKind::Boolean),
430];
431pub(super) fn validate(body: Option<&JsValue>) -> AuthResult<JsValue> {
432 validate_metadata(body, "body", FIELDS)
433}
434fn validate_metadata(
435 body: Option<&JsValue>,
436 location: &str,
437 fields: &[(&str, FieldKind)],
438) -> AuthResult<JsValue> {
439 let Some(object) = body.and_then(JsValue::as_object) else {
440 return Err(crate::endpoint::validation(format!(
441 "[{location}] Invalid input: expected object, received {}",
442 crate::authentication_helpers::json_type(body)
443 )));
444 };
445 let mut issues = Vec::new();
446 let mut output = indexmap::IndexMap::new();
447 for (name, kind) in fields {
448 let Some(value) = object.get(*name) else {
449 continue;
450 };
451 let path = format!("{location}.{name}");
452 let normalized = match (kind, value) {
453 (FieldKind::Uri | FieldKind::Method, JsValue::String(value)) => {
455 JsValue::String(js_trim(value).into())
456 }
457 (FieldKind::Uris, JsValue::Array(values)) => JsValue::Array(
458 values
459 .iter()
460 .map(|value| {
461 value.as_str().map_or_else(
462 || value.clone(),
463 |text| JsValue::String(js_trim(text).into()),
464 )
465 })
466 .collect(),
467 ),
468 (FieldKind::Grants | FieldKind::Scopes, JsValue::Array(values)) => JsValue::Array(
469 values
470 .iter()
471 .map(|value| {
472 value.as_str().map_or_else(
473 || value.clone(),
474 |text| JsValue::String(js_trim(text).into()),
475 )
476 })
477 .collect(),
478 ),
479 (FieldKind::Jwks, JsValue::Object(object)) => JsValue::Object(
480 object
481 .iter()
482 .filter(|(name, _)| name.as_str() == "keys")
483 .map(|(name, value)| (name.clone(), value.clone()))
484 .collect(),
485 ),
486 _ => value.clone(),
487 };
488 validate_field(*kind, &normalized, &path, &mut issues);
489 _ = output.insert((*name).into(), normalized);
490 }
491 if !issues.is_empty() {
492 return Err(crate::endpoint::validation(issues.join("; ")));
493 }
494 Ok(JsValue::Object(output))
495}
496fn validate_field(kind: FieldKind, value: &JsValue, path: &str, issues: &mut Vec<String>) {
497 use crate::authentication_helpers::{JsonFieldKind, field_issue, json_type};
498 let mut add = |issue: String| issues.push(format!("[{path}] {issue}"));
499 match kind {
500 FieldKind::String | FieldKind::Method | FieldKind::Uri => {
501 if let Some(issue) =
502 field_issue(JsonFieldKind::String, Some(value), json_type(Some(value)))
503 {
504 add(issue);
505 return;
506 }
507 if let Some(text) = value.as_str() {
508 if matches!(kind, FieldKind::Method) && js_trim(text).is_empty() {
509 add("Too small: expected string to have >=1 characters".into());
510 }
511 if matches!(kind, FieldKind::Uri) {
512 for issue in safe_uri_issues(text) {
513 add(issue.into());
514 }
515 }
516 }
517 }
518 FieldKind::Boolean => {
519 if let Some(issue) =
520 field_issue(JsonFieldKind::Boolean, Some(value), json_type(Some(value)))
521 {
522 add(issue);
523 }
524 }
525 FieldKind::Application => {
526 if ![Some("web"), Some("native")].contains(&value.as_str()) {
527 add("Invalid option: expected one of \"web\"|\"native\"".into());
528 }
529 }
530 FieldKind::Uris
531 | FieldKind::Contacts
532 | FieldKind::Grants
533 | FieldKind::Responses
534 | FieldKind::Scopes
535 | FieldKind::Resources => {
536 let JsValue::Array(values) = value else {
537 add(format!(
538 "Invalid input: expected array, received {}",
539 json_type(Some(value))
540 ));
541 return;
542 };
543 if values.is_empty()
544 && !matches!(
545 kind,
546 FieldKind::Responses | FieldKind::Scopes | FieldKind::Resources
547 )
548 {
549 add("Too small: expected array to have >=1 items".into());
550 }
551 for (index, value) in values.iter().enumerate() {
552 let item_path = format!("{path}.{index}");
553 if matches!(kind, FieldKind::Responses) {
554 if value.as_str() != Some("code") {
555 issues.push(format!("[{item_path}] Invalid input: expected \"code\""));
556 }
557 continue;
558 }
559 if let Some(issue) =
560 field_issue(JsonFieldKind::String, Some(value), json_type(Some(value)))
561 {
562 issues.push(format!("[{item_path}] {issue}"));
563 continue;
564 }
565 if let Some(text) = value.as_str() {
566 if matches!(kind, FieldKind::Contacts) && text.is_empty()
567 || matches!(kind, FieldKind::Grants | FieldKind::Scopes)
568 && js_trim(text).is_empty()
569 {
570 issues.push(format!(
571 "[{item_path}] Too small: expected string to have >=1 characters"
572 ));
573 }
574 if matches!(kind, FieldKind::Uris) {
575 for issue in safe_uri_issues(text) {
576 issues.push(format!("[{item_path}] {issue}"));
577 }
578 }
579 if matches!(kind, FieldKind::Resources)
580 && let Some(issue) = super::resources::resource_uri_issue(text)
581 {
582 issues.push(format!("[{item_path}] {issue}"));
583 }
584 }
585 }
586 }
587 FieldKind::Subject => {
588 if ![Some("public"), Some("pairwise")].contains(&value.as_str()) {
589 add("Invalid option: expected one of \"public\"|\"pairwise\"".into());
590 }
591 }
592 FieldKind::Expiration => {
593 if !matches!(value, JsValue::String(_) | JsValue::Number(_)) {
594 add("Invalid input".into());
595 }
596 }
597 FieldKind::Record => {
598 if value.as_object().is_none() {
599 add(format!(
600 "Invalid input: expected record, received {}",
601 json_type(Some(value))
602 ));
603 }
604 }
605 FieldKind::Jwks => {
606 let Some(object) = value.as_object() else {
607 add(format!(
608 "Invalid input: expected object, received {}",
609 json_type(Some(value))
610 ));
611 return;
612 };
613 let keys = object.get("keys");
614 let Some(JsValue::Array(values)) = keys else {
615 issues.push(format!(
616 "[{path}.keys] Invalid input: expected array, received {}",
617 json_type(keys)
618 ));
619 return;
620 };
621 if values.is_empty() {
622 issues.push(format!(
623 "[{path}.keys] Too small: expected array to have >=1 items"
624 ));
625 }
626 for (index, value) in values.iter().enumerate() {
627 if !matches!(value, JsValue::Object(_)) {
628 issues.push(format!(
629 "[{path}.keys.{index}] Invalid input: expected record, received {}",
630 json_type(Some(value))
631 ));
632 }
633 }
634 }
635 }
636}
637pub(super) fn safe_uri_issues(uri: &str) -> Vec<&'static str> {
638 let Ok(url) = url::Url::parse(uri) else {
639 return vec!["Invalid URL"];
640 };
641 if ["javascript", "data", "vbscript"].contains(&url.scheme()) {
642 return vec!["URL cannot use javascript:, data:, or vbscript: scheme"];
643 }
644 let mut issues = Vec::new();
645 if uri.contains('#') {
646 issues.push("Redirect URI must not contain a fragment component");
647 }
648 let host = url
649 .host_str()
650 .unwrap_or_default()
651 .trim_matches(['[', ']'])
652 .trim_end_matches('.');
653 let loopback = host == "localhost"
654 || host.ends_with(".localhost")
655 || host.parse::<std::net::IpAddr>().is_ok_and(|ip| match ip {
656 std::net::IpAddr::V4(ip) => ip.is_loopback(),
657 std::net::IpAddr::V6(ip) => {
658 ip.is_loopback() || ip.to_ipv4_mapped().is_some_and(|ip| ip.is_loopback())
659 }
660 });
661 if url.scheme() == "http" && !loopback {
662 issues.push("Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)");
663 }
664 issues
665}
666
667#[derive(Debug, Deserialize)]
668pub(super) struct ReadClientInput {
669 pub client_id: String,
670}
671pub(super) fn validate_read(query: Option<&JsValue>) -> AuthResult<JsValue> {
672 crate::endpoint::validate_fields(
673 query,
674 "query",
675 &[crate::authentication_helpers::JsonField::string(
676 "client_id",
677 true,
678 )],
679 )
680}
681impl OAuthProviderPlugin {
682 pub fn get_oauth_client_endpoint(client_id: &str) -> AuthResult<ServerEndpoint<Value>> {
684 ServerEndpoint::new("oauth-provider", "getOAuthClient")
685 .with_query(&json!({"client_id":client_id}))
686 }
687 pub(super) async fn read_client<S: AuthSchema>(
688 &self,
689 input: ReadClientInput,
690 req: &impl SessionRequest,
691 ctx: &AuthContext<S>,
692 ) -> AuthResult<EndpointResponse> {
693 let (user, _) = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
694 if let Some(response) = self
695 .require_client_privileges("read", Some(&input.client_id), req, ctx)
696 .await?
697 {
698 return Ok(response);
699 }
700 let Some(client) = super::discovery::resolve(ctx, &input.client_id).await? else {
701 return Ok(EndpointResponse::error(AuthError::Api {
702 status: 404,
703 code: None,
704 message: String::new(),
705 })
706 .with_error_body(alibi_core::utils::json::parse_value(
707 r#"{"error":"not_found","error_description":"client not found"}"#,
708 )?));
709 };
710 if client.user_id.as_deref() != Some(user.id().as_ref()) {
711 return Ok(EndpointResponse::error(AuthError::Api {
712 status: 401,
713 code: None,
714 message: String::new(),
715 })
716 .without_error_body()
717 .with_header("Content-Type", "application/json"));
718 }
719 EndpointResponse::json(&client_read_metadata(client)?)
720 }
721}
722
723impl OAuthProviderPlugin {
724 pub fn get_oauth_client_public_endpoint(client_id: &str) -> AuthResult<ServerEndpoint<Value>> {
726 ServerEndpoint::new("oauth-provider", "getOAuthClientPublic")
727 .with_query(&json!({"client_id":client_id}))
728 }
729 pub(super) async fn read_public_client<S: AuthSchema>(
730 &self,
731 input: ReadClientInput,
732 req: &impl SessionRequest,
733 ctx: &AuthContext<S>,
734 ) -> AuthResult<EndpointResponse> {
735 _ = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
736 Box::pin(self.public_client_projection(&input.client_id, ctx)).await
737 }
738 async fn public_client_projection<S: AuthSchema>(
739 &self,
740 client_id: &str,
741 ctx: &AuthContext<S>,
742 ) -> AuthResult<EndpointResponse> {
743 let client = super::discovery::resolve(ctx, client_id).await?;
744
745 let Some(client) =
746 client.filter(|client| client.metadata.get("disabled") != Some(&json!(true)))
747 else {
748 return Ok(EndpointResponse::error(AuthError::Api {
749 status: 404,
750 code: None,
751 message: String::new(),
752 })
753 .with_error_body(alibi_core::utils::json::parse_value(
754 r#"{"error":"not_found","error_description":"client not found"}"#,
755 )?));
756 };
757 let mut display = serde_json::Map::new();
758 for name in [
759 "client_name",
760 "client_uri",
761 "logo_uri",
762 "contacts",
763 "tos_uri",
764 "policy_uri",
765 ] {
766 if let Some(value) = client.metadata.get(name) {
767 _ = display.insert(name.into(), value.clone());
768 }
769 }
770 _ = display.insert("client_id".into(), json!(client.client_id));
771 _ = display.insert("redirect_uris".into(), json!([]));
772 EndpointResponse::json(&Value::Object(display))
773 }
774}
775
776fn client_read_metadata(client: alibi_core::store::OAuthClient) -> AuthResult<Value> {
777 let mut output = client_response_metadata(serde_json::from_value(client.metadata)?);
778 _ = output.insert("client_id".into(), json!(client.client_id));
779 _ = output.insert("user_id".into(), json!(client.user_id));
780 _ = output.insert(
781 "client_id_issued_at".into(),
782 json!(client.created_at.map(|time| time.timestamp())),
783 );
784 if client.client_secret.is_some() {
785 _ = output
786 .entry("client_secret_expires_at")
787 .or_insert_with(|| json!(0));
788 }
789 _ = output.remove("client_secret");
790 _ = output.remove("client_credentials_scopes");
791 Ok(Value::Object(output))
792}
793
794fn client_response_metadata(
797 mut metadata: serde_json::Map<String, Value>,
798) -> serde_json::Map<String, Value> {
799 if let Some(Value::Object(extensions)) = metadata.remove("metadata") {
800 metadata.extend(
801 extensions
802 .into_iter()
803 .filter(|(name, _)| !RESERVED_METADATA_FIELDS.contains(&name.as_str())),
804 );
805 }
806 metadata
807}
808impl OAuthProviderPlugin {
809 #[must_use]
811 pub fn get_oauth_clients_endpoint() -> ServerEndpoint<Value> {
812 ServerEndpoint::new("oauth-provider", "getOAuthClients")
813 }
814 pub(super) async fn list_clients<S: AuthSchema>(
815 &self,
816 req: &impl SessionRequest,
817 ctx: &AuthContext<S>,
818 ) -> AuthResult<EndpointResponse> {
819 let (user, _) = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
820 if let Some(response) = self
821 .require_client_privileges("list", None, req, ctx)
822 .await?
823 {
824 return Ok(response);
825 }
826 let clients = ctx
827 .database
828 .list_oauth_clients_by_user(user.id().as_ref())
829 .await?;
830 let output: Vec<Value> = clients
831 .into_iter()
832 .map(client_read_metadata)
833 .collect::<AuthResult<_>>()?;
834 EndpointResponse::json(&output)
835 }
836}
837
838impl OAuthProviderPlugin {
839 pub fn delete_oauth_client_endpoint(client_id: &str) -> AuthResult<ServerEndpoint<()>> {
841 ServerEndpoint::new("oauth-provider", "deleteOAuthClient")
842 .with_body(&json!({"client_id":client_id}))
843 }
844 pub(super) async fn delete_client<S: AuthSchema>(
845 &self,
846 input: ReadClientInput,
847 req: &impl SessionRequest,
848 ctx: &AuthContext<S>,
849 ) -> AuthResult<EndpointResponse> {
850 let (user, _) = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
851 if let Some(response) = self
852 .require_client_privileges("delete", Some(&input.client_id), req, ctx)
853 .await?
854 {
855 return Ok(response);
856 }
857 if self
858 .config
859 .cached_trusted_clients
860 .contains(&input.client_id)
861 {
862 return status_protocol_error(
863 400,
864 "invalid_client",
865 "trusted clients must be updated manually",
866 );
867 }
868 let Some(client) = super::discovery::resolve(ctx, &input.client_id).await? else {
869 return Ok(EndpointResponse::error(AuthError::Api {
870 status: 404,
871 code: None,
872 message: String::new(),
873 })
874 .with_error_body(alibi_core::utils::json::parse_value(
875 r#"{"error":"not_found","error_description":"client not found"}"#,
876 )?));
877 };
878 if client.user_id.as_deref() != Some(user.id().as_ref()) {
879 return Ok(EndpointResponse::error(AuthError::Api {
880 status: 401,
881 code: None,
882 message: String::new(),
883 })
884 .without_error_body()
885 .with_header("Content-Type", "application/json"));
886 }
887 ctx.database.delete_oauth_client(&input.client_id).await?;
888 Ok(EndpointResponse::empty().with_header("Content-Type", "application/json"))
889 }
890}
891pub(super) fn validate_delete(body: Option<&JsValue>) -> AuthResult<JsValue> {
892 crate::endpoint::validate_fields(
893 body,
894 "body",
895 &[crate::authentication_helpers::JsonField::string(
896 "client_id",
897 true,
898 )],
899 )
900}
901
902impl OAuthProviderPlugin {
903 pub fn rotate_client_secret_endpoint(client_id: &str) -> AuthResult<ServerEndpoint<Value>> {
905 ServerEndpoint::new("oauth-provider", "rotateClientSecret")
906 .with_body(&json!({"client_id":client_id}))
907 }
908 pub(super) async fn rotate_client_secret<S: AuthSchema>(
909 &self,
910 input: ReadClientInput,
911 req: &impl SessionRequest,
912 ctx: &AuthContext<S>,
913 ) -> AuthResult<EndpointResponse> {
914 let (user, _) = Box::pin(crate::helpers::ordinary_session(req, ctx)).await?;
915 req.queue_response_header("Cache-Control", "no-store");
916 req.queue_response_header("Pragma", "no-cache");
917 if let Some(response) = self
918 .require_client_privileges("rotate", Some(&input.client_id), req, ctx)
919 .await?
920 {
921 return Ok(response);
922 }
923 if self
924 .config
925 .cached_trusted_clients
926 .contains(&input.client_id)
927 {
928 return status_protocol_error(
929 400,
930 "invalid_client",
931 "trusted clients must be updated manually",
932 );
933 }
934 let Some(client) = super::discovery::resolve(ctx, &input.client_id).await? else {
935 return status_protocol_error(404, "not_found", "client not found");
936 };
937 if client.user_id.as_deref() != Some(user.id().as_ref()) {
938 return Ok(EndpointResponse::error(AuthError::Api {
939 status: 401,
940 code: None,
941 message: String::new(),
942 })
943 .without_error_body()
944 .with_header("Content-Type", "application/json"));
945 }
946 if client
947 .metadata
948 .get("token_endpoint_auth_method")
949 .and_then(Value::as_str)
950 == Some("none")
951 || client.client_secret.is_none()
952 {
953 return status_protocol_error(
954 400,
955 "invalid_client",
956 "secret rotation is only available for clients using client_secret authentication",
957 );
958 }
959 let plaintext = self.generate_client_secret()?;
960 let protected = super::client_secret::store(ctx, &self.config, &plaintext).await?;
961 let now = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
962 .ok_or_else(|| AuthError::internal("Invalid client update timestamp"))?;
963 let Some(client) = ctx
964 .database
965 .update_oauth_client_secret(&input.client_id, protected, now)
966 .await?
967 else {
968 return status_protocol_error(500, "invalid_client", "unable to update client");
969 };
970 let mut output = client_read_metadata(client)?;
971 _ = output
972 .as_object_mut()
973 .ok_or_else(|| AuthError::internal("OAuth client metadata must be an object"))?
974 .insert(
975 "client_secret".into(),
976 Value::String(self.prefixed_client_secret(plaintext)),
977 );
978 EndpointResponse::json(&output)
979 }
980}
981pub(super) fn status_protocol_error(
982 status: u16,
983 error: &str,
984 description: &str,
985) -> AuthResult<EndpointResponse> {
986 Ok(EndpointResponse::error(AuthError::Api {
987 status,
988 code: None,
989 message: String::new(),
990 })
991 .with_error_body(alibi_core::utils::json::parse_value(
992 &serde_json::to_string(&json!({"error":error,"error_description":description}))?,
993 )?))
994}
995
996impl OAuthProviderPlugin {
997 fn admit_metadata<S: AuthSchema>(
998 &self,
999 metadata: serde_json::Map<String, Value>,
1000 allowed_scopes: &[String],
1001 ctx: &AuthContext<S>,
1002 ) -> AuthResult<Result<serde_json::Map<String, Value>, EndpointResponse>> {
1003 self.admit_metadata_for_source(metadata, allowed_scopes, ctx, false)
1004 }
1005 pub(super) fn admit_metadata_for_source<S: AuthSchema>(
1006 &self,
1007 mut metadata: serde_json::Map<String, Value>,
1008 allowed_scopes: &[String],
1009 ctx: &AuthContext<S>,
1010 document: bool,
1011 ) -> AuthResult<Result<serde_json::Map<String, Value>, EndpointResponse>> {
1012 let auth = metadata
1013 .get("token_endpoint_auth_method")
1014 .and_then(Value::as_str)
1015 .unwrap_or("client_secret_basic")
1016 .to_owned();
1017 if !self.supported_auth_methods(true).contains(&auth) {
1018 return protocol_error(
1019 "invalid_client_metadata",
1020 format!("unsupported token_endpoint_auth_method {auth}"),
1021 )
1022 .map(Err);
1023 }
1024 let grants = metadata
1025 .get("grant_types")
1026 .and_then(Value::as_array)
1027 .cloned()
1028 .unwrap_or_else(|| vec![json!("authorization_code")]);
1029 if document && grants.is_empty() {
1030 return protocol_error(
1031 "invalid_client_metadata",
1032 "grant_types must contain at least one grant type",
1033 )
1034 .map(Err);
1035 }
1036 if document
1037 && !grants.iter().any(|grant| {
1038 grant.as_str().is_some_and(|grant| {
1039 self.supported_grant_types()
1040 .iter()
1041 .any(|supported| supported == grant)
1042 })
1043 })
1044 {
1045 return protocol_error(
1046 "invalid_client_metadata",
1047 format!(
1048 "no mutually supported grant_type among {}",
1049 grants
1050 .iter()
1051 .filter_map(Value::as_str)
1052 .collect::<Vec<_>>()
1053 .join(", ")
1054 ),
1055 )
1056 .map(Err);
1057 }
1058 for grant in &grants {
1059 if !document
1060 && !grant.as_str().is_some_and(|grant| {
1061 self.supported_grant_types()
1062 .iter()
1063 .any(|supported| supported == grant)
1064 })
1065 {
1066 return protocol_error(
1067 "invalid_client_metadata",
1068 format!(
1069 "unsupported grant_type {}",
1070 grant.as_str().unwrap_or_default()
1071 ),
1072 )
1073 .map(Err);
1074 }
1075 }
1076 let code_grant = grants.contains(&json!("authorization_code"));
1077 let redirects = metadata
1078 .get("redirect_uris")
1079 .and_then(Value::as_array)
1080 .cloned()
1081 .unwrap_or_default();
1082 if code_grant && redirects.is_empty() {
1083 return protocol_error(
1084 "invalid_redirect_uri",
1085 "Redirect URIs are required for authorization_code and implicit grant types",
1086 )
1087 .map(Err);
1088 }
1089 let application = metadata
1090 .get("application_type")
1091 .and_then(Value::as_str)
1092 .unwrap_or(if document { "native" } else { "web" });
1093 if document && !["web", "native"].contains(&application) {
1094 return protocol_error(
1095 "invalid_client_metadata",
1096 "application_type must be web or native",
1097 )
1098 .map(Err);
1099 }
1100 for redirect in &redirects {
1101 if let Some(error) = redirect_error(redirect.as_str().unwrap_or_default(), application)
1102 {
1103 return protocol_error("invalid_redirect_uri", error).map(Err);
1104 }
1105 }
1106 let responses = metadata
1107 .get("response_types")
1108 .and_then(Value::as_array)
1109 .cloned()
1110 .or_else(|| code_grant.then(|| vec![json!("code")]));
1111 if code_grant
1112 && !responses
1113 .as_ref()
1114 .is_some_and(|responses| responses.contains(&json!("code")))
1115 {
1116 return protocol_error(
1117 "invalid_client_metadata",
1118 "When 'authorization_code' grant type is used, 'code' response type must be included",
1119 ).map(Err);
1120 }
1121 if !code_grant
1122 && responses
1123 .as_ref()
1124 .is_some_and(|responses| responses.contains(&json!("code")))
1125 {
1126 return protocol_error(
1127 "invalid_client_metadata",
1128 "When 'code' response type is used, 'authorization_code' grant type must be included",
1129 ).map(Err);
1130 }
1131 if document
1132 && metadata
1133 .get("subject_type")
1134 .and_then(Value::as_str)
1135 .is_some_and(|subject| !["public", "pairwise"].contains(&subject))
1136 {
1137 return protocol_error(
1138 "invalid_client_metadata",
1139 "subject_type must be \"public\" or \"pairwise\"",
1140 )
1141 .map(Err);
1142 }
1143 if metadata.get("subject_type").and_then(Value::as_str) == Some("pairwise") {
1144 if self
1145 .config
1146 .pairwise_secret
1147 .as_ref()
1148 .is_none_or(String::is_empty)
1149 {
1150 return protocol_error(
1151 "invalid_client_metadata",
1152 "pairwise subject_type requires server pairwiseSecret configuration",
1153 )
1154 .map(Err);
1155 }
1156 if redirects.iter().filter_map(Value::as_str).any(|uri| {
1159 url::Url::parse(uri).is_ok_and(|url| {
1160 !["http", "https"].contains(&url.scheme())
1161 && url.host_str().is_some_and(|host| !host.is_empty())
1162 })
1163 }) {
1164 return protocol_error(
1165 "invalid_client_metadata",
1166 "pairwise clients cannot use custom-scheme redirect URIs that include a host",
1167 )
1168 .map(Err);
1169 }
1170 let mut hosts = Vec::new();
1171 for redirect in &redirects {
1172 if let Some(uri) = redirect.as_str()
1173 && let Ok(url) = url::Url::parse(uri)
1174 {
1175 let host = format!(
1176 "{}:{}",
1177 url.host_str().unwrap_or_default(),
1178 url.port().map_or_else(String::new, |port| port.to_string())
1179 );
1180 if !hosts.contains(&host) {
1181 hosts.push(host);
1182 }
1183 }
1184 }
1185 if hosts.len() > 1 {
1186 return protocol_error("invalid_client_metadata","pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host.").map(Err);
1187 }
1188 }
1189 if let Some(scope) = metadata.get("scope").and_then(Value::as_str) {
1190 for scope in scope.split(' ').filter(|scope| !scope.is_empty()) {
1191 if !allowed_scopes.iter().any(|allowed| allowed == scope) {
1192 return protocol_error(
1193 "invalid_scope",
1194 format!("cannot request scope {scope}"),
1195 )
1196 .map(Err);
1197 }
1198 }
1199 }
1200 if metadata.contains_key("jwks") && metadata.contains_key("jwks_uri") {
1201 return protocol_error(
1202 "invalid_client_metadata",
1203 "jwks and jwks_uri are mutually exclusive",
1204 )
1205 .map(Err);
1206 }
1207 if document && metadata.get("require_pkce") == Some(&json!(false)) {
1208 return protocol_error(
1209 "invalid_client_metadata",
1210 "pkce is required for registered clients.",
1211 )
1212 .map(Err);
1213 }
1214 if let Some(uri) = metadata.get("jwks_uri").and_then(Value::as_str)
1215 && let Some(error) = remote_key_uri_error(uri, ctx).filter(|error| {
1216 !(document
1217 && error.starts_with("jwks_uri must belong")
1218 && metadata
1219 .get("client_id")
1220 .and_then(Value::as_str)
1221 .and_then(|id| url::Url::parse(id).ok())
1222 .zip(url::Url::parse(uri).ok())
1223 .is_some_and(|(id, key)| id.origin() == key.origin()))
1224 })
1225 {
1226 return protocol_error("invalid_client_metadata", error).map(Err);
1227 }
1228 if let Some(jwks) = metadata.get("jwks")
1229 && document
1230 && !jwks
1231 .get("keys")
1232 .and_then(Value::as_array)
1233 .is_some_and(|keys| !keys.is_empty() && keys.iter().all(Value::is_object))
1234 {
1235 return protocol_error(
1236 "invalid_client_metadata",
1237 "jwks must be an RFC 7517 JWK Set object with a non-empty keys array",
1238 )
1239 .map(Err);
1240 }
1241 if let Some(jwks) = metadata.get("jwks")
1242 && let Some(error) = public_jwks_error(jwks)
1243 {
1244 return protocol_error("invalid_client_metadata", error).map(Err);
1245 }
1246 if auth == "private_key_jwt"
1247 && !metadata.contains_key("jwks")
1248 && !metadata.contains_key("jwks_uri")
1249 {
1250 return protocol_error(
1251 "invalid_client_metadata",
1252 "private_key_jwt requires either jwks or jwks_uri",
1253 )
1254 .map(Err);
1255 }
1256 if let Some(uri) = metadata
1257 .get("backchannel_logout_uri")
1258 .and_then(Value::as_str)
1259 {
1260 if self.config.disable_jwt_plugin {
1261 return protocol_error(
1262 "invalid_client_metadata",
1263 "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)",
1264 ).map(Err);
1265 }
1266 if let Some(error) = backchannel_uri_error(uri) {
1267 return protocol_error("invalid_client_metadata", error).map(Err);
1268 }
1269 }
1270 _ = metadata.insert("token_endpoint_auth_method".into(), json!(auth));
1271 if !document {
1272 _ = metadata
1273 .entry("application_type")
1274 .or_insert_with(|| json!("web"));
1275 }
1276 _ = metadata.insert("grant_types".into(), json!(grants));
1277 if let Some(responses) = responses {
1278 _ = metadata.insert("response_types".into(), json!(responses));
1279 }
1280 _ = metadata.insert("redirect_uris".into(), json!(redirects));
1281 if !document {
1282 _ = metadata.entry("disabled").or_insert_with(|| json!(false));
1283 _ = metadata
1284 .entry("dpop_bound_access_tokens")
1285 .or_insert_with(|| json!(false));
1286 }
1287 Ok(Ok(metadata))
1288 }
1289}
1290
1291#[derive(Debug, Deserialize)]
1292pub(super) struct UpdateClientInput {
1293 client_id: String,
1294 update: Value,
1295}
1296pub(super) fn validate_update(body: Option<&JsValue>) -> AuthResult<JsValue> {
1297 let mut output = crate::endpoint::validate_fields(
1298 body,
1299 "body",
1300 &[crate::authentication_helpers::JsonField::string(
1301 "client_id",
1302 true,
1303 )],
1304 )?;
1305 let fields = FIELDS
1306 .iter()
1307 .copied()
1308 .filter(|(name, _)| {
1309 ![
1310 "token_endpoint_auth_method",
1311 "jwks",
1312 "jwks_uri",
1313 "dpop_bound_access_tokens",
1314 ]
1315 .contains(name)
1316 })
1317 .collect::<Vec<_>>();
1318 let update = validate_metadata(
1319 body.and_then(|value| value.get("update")),
1320 "body.update",
1321 &fields,
1322 )?;
1323 if let JsValue::Object(object) = &mut output {
1324 _ = object.insert("update".into(), update);
1325 }
1326 Ok(output)
1327}
1328impl OAuthProviderPlugin {
1329 pub fn update_oauth_client_endpoint(
1331 client_id: &str,
1332 update: &impl Serialize,
1333 ) -> AuthResult<ServerEndpoint<Value>> {
1334 ServerEndpoint::new("oauth-provider", "updateOAuthClient")
1335 .with_body(&json!({"client_id":client_id,"update":update}))
1336 }
1337
1338 pub(super) async fn update_client<S: AuthSchema>(
1339 &self,
1340 input: UpdateClientInput,
1341 req: &impl SessionRequest,
1342 ctx: &AuthContext<S>,
1343 admin: bool,
1344 ) -> AuthResult<EndpointResponse> {
1345 let user = if admin {
1346 let Some((user, _)) = Box::pin(ctx.require_cached_session(req)).await.ok() else {
1347 return Ok(EndpointResponse::error(AuthError::Api {
1348 status: 401,
1349 code: None,
1350 message: String::new(),
1351 })
1352 .without_error_body()
1353 .with_header("Content-Type", "application/json"));
1354 };
1355 user
1356 } else {
1357 Box::pin(crate::helpers::ordinary_session(req, ctx))
1358 .await?
1359 .0
1360 };
1361 if let Some(response) = self
1362 .require_client_privileges("update", Some(&input.client_id), req, ctx)
1363 .await?
1364 {
1365 return Ok(response);
1366 }
1367 if self
1368 .config
1369 .cached_trusted_clients
1370 .contains(&input.client_id)
1371 {
1372 return status_protocol_error(
1373 400,
1374 "invalid_client",
1375 "trusted clients must be updated manually",
1376 );
1377 }
1378 let Some(client) = super::discovery::resolve(ctx, &input.client_id).await? else {
1379 return status_protocol_error(404, "not_found", "client not found");
1380 };
1381 let mut updates: serde_json::Map<String, Value> = serde_json::from_value(input.update)?;
1382 let cross_owner_scopes = client.user_id.as_deref() != Some(user.id().as_ref())
1385 && admin
1386 && self.config.client_privileges.is_some()
1387 && updates.len() == 1
1388 && updates.contains_key("client_credentials_scopes");
1389 if client.user_id.as_deref() != Some(user.id().as_ref()) && !cross_owner_scopes {
1390 return Ok(client_privilege_denied(401));
1391 }
1392 if cross_owner_scopes
1393 && let Some(response) = self
1394 .require_client_privileges(
1395 "configure-client-credentials-scopes",
1396 Some(&input.client_id),
1397 req,
1398 ctx,
1399 )
1400 .await?
1401 {
1402 return Ok(response);
1403 }
1404 if updates.is_empty() {
1405 return client_update_response(client, admin);
1406 }
1407 let mut merged: serde_json::Map<String, Value> = serde_json::from_value(client.metadata)?;
1408 let scopes = updates.remove("client_credentials_scopes");
1409 let extensions = updates
1410 .remove("metadata")
1411 .and_then(|value| value.as_object().cloned());
1412 _ = updates.remove("client_secret_expires_at");
1414 merged.extend(updates);
1415 if let Some(scopes) = scopes.and_then(|value| value.as_array().cloned()) {
1416 if let Some(response) = self
1417 .admit_credential_scopes(
1418 &scopes,
1419 &merged,
1420 Some(&input.client_id),
1421 req,
1422 ctx,
1423 !cross_owner_scopes,
1424 )
1425 .await?
1426 {
1427 return Ok(response);
1428 }
1429 _ = merged.insert("client_credentials_scopes".into(), json!(scopes));
1430 }
1431 if let Some(extensions) = extensions {
1432 let extensions = extensions
1433 .into_iter()
1434 .filter(|(name, _)| !RESERVED_METADATA_FIELDS.contains(&name.as_str()))
1435 .collect::<serde_json::Map<_, _>>();
1436 if !extensions.is_empty() {
1437 merged.retain(|name, _| {
1438 RESERVED_METADATA_FIELDS.contains(&name.as_str())
1439 || name == "client_discovery_id"
1440 });
1441 _ = merged.insert("metadata".into(), Value::Object(extensions));
1442 }
1443 }
1444 if !merged
1445 .get("grant_types")
1446 .and_then(Value::as_array)
1447 .is_some_and(|grants| grants.contains(&json!("client_credentials")))
1448 || merged
1449 .get("token_endpoint_auth_method")
1450 .and_then(Value::as_str)
1451 == Some("none")
1452 {
1453 _ = merged.insert("client_credentials_scopes".into(), json!([]));
1454 }
1455 let metadata = match self.admit_metadata(merged.clone(), &self.config.scopes, ctx)? {
1457 Ok(_) => Value::Object(merged),
1458 Err(response) => return Ok(response),
1459 };
1460 let now = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
1461 .ok_or_else(|| AuthError::internal("Invalid client update timestamp"))?;
1462 let Some(client) = ctx
1463 .database
1464 .update_oauth_client_metadata(&input.client_id, metadata, now)
1465 .await?
1466 else {
1467 return status_protocol_error(500, "invalid_client", "unable to update client");
1468 };
1469 client_update_response(client, admin)
1470 }
1471}
1472
1473impl OAuthProviderPlugin {
1474 fn generate_client_secret(&self) -> AuthResult<String> {
1475 let generated = self
1476 .config
1477 .client_secret_generator
1478 .as_ref()
1479 .map(|generator| generator.generate())
1480 .transpose()?;
1481 Ok(generated
1482 .filter(|secret| !secret.is_empty())
1483 .unwrap_or_else(random_credential))
1484 }
1485 fn prefixed_client_secret(&self, secret: String) -> String {
1486 match self.config.client_secret_prefix.as_deref() {
1487 Some(prefix) if !prefix.is_empty() => format!("{prefix}{secret}"),
1488 _ => secret,
1489 }
1490 }
1491 async fn persist_client<S: AuthSchema>(
1492 &self,
1493 mut metadata: serde_json::Map<String, Value>,
1494 user_id: Option<String>,
1495 dynamic: bool,
1496 ctx: &AuthContext<S>,
1497 ) -> AuthResult<EndpointResponse> {
1498 let auth = metadata
1499 .get("token_endpoint_auth_method")
1500 .and_then(Value::as_str)
1501 .unwrap_or("client_secret_basic");
1502 let client_id = match &self.config.client_id_generator {
1503 Some(generator) => generator.generate()?,
1504 None => random_credential(),
1505 };
1506 let plaintext =
1507 if ["none", "private_key_jwt"].contains(&auth) || self.is_extension_auth_method(auth) {
1508 None
1509 } else {
1510 Some(self.generate_client_secret()?)
1511 };
1512 let stored = if let Some(secret) = plaintext.as_deref() {
1513 Some(super::client_secret::store(ctx, &self.config, secret).await?)
1514 } else {
1515 None
1516 };
1517 let now = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
1518 .ok_or_else(|| AuthError::internal("Invalid client creation timestamp"))?;
1519 let requested = metadata
1524 .remove("resources")
1525 .map(serde_json::from_value::<Vec<String>>)
1526 .transpose()?
1527 .unwrap_or_default();
1528 let defaults = self
1529 .config
1530 .client_registration_default_resources
1531 .as_deref()
1532 .unwrap_or_default();
1533 for identifier in requested.iter().filter(|_| dynamic) {
1534 if !defaults.contains(identifier)
1535 && !self
1536 .config
1537 .client_registration_allowed_resources
1538 .as_ref()
1539 .is_some_and(|allowed| allowed.contains(identifier))
1540 {
1541 return protocol_error(
1542 "invalid_target",
1543 format!(
1544 "requested resource {identifier} is not allowed for client registration"
1545 ),
1546 );
1547 }
1548 }
1549 let mut resources = Vec::new();
1550 for identifier in defaults.iter().chain(&requested) {
1551 if !resources.contains(identifier) {
1552 resources.push(identifier.clone());
1553 }
1554 }
1555 for identifier in &resources {
1556 let Some(resource) = self.get_resource(ctx, identifier).await? else {
1557 return protocol_error(
1558 "invalid_target",
1559 format!("requested resource {identifier} does not exist"),
1560 );
1561 };
1562 if resource.metadata.get("disabled").and_then(Value::as_bool) == Some(true) {
1563 return protocol_error(
1564 "invalid_target",
1565 format!("requested resource {identifier} is disabled"),
1566 );
1567 }
1568 }
1569 let administrative = metadata.contains_key("client_credentials_scopes");
1570 _ = metadata
1571 .entry("client_credentials_scopes")
1572 .or_insert_with(|| json!([]));
1573 let client = ctx
1574 .database
1575 .persist_oauth_client_registration(PersistOAuthClientRegistration {
1576 client: CreateOAuthClient {
1577 client_id,
1578 user_id,
1579 client_secret: stored,
1580 metadata: Value::Object(metadata),
1581 created_at: now,
1582 },
1583 replacement: None,
1584 resources: resources.clone(),
1585 updated_at: now,
1586 })
1587 .await
1588 .map_err(|error| match error {
1589 OAuthClientRegistrationWriteError::Failure(error)
1590 | OAuthClientRegistrationWriteError::Collision { cause: error, .. } => error,
1591 })?
1592 .ok_or_else(|| AuthError::internal("Client creation returned no client"))?;
1593 let mut output = client_response_metadata(serde_json::from_value(client.metadata)?);
1594 if !administrative {
1595 _ = output.remove("client_credentials_scopes");
1596 }
1597 if !resources.is_empty() {
1598 _ = output.insert("resources".into(), json!(resources));
1599 }
1600 _ = output.insert("client_id".into(), json!(client.client_id));
1601 if let Some(user_id) = client.user_id {
1602 _ = output.insert("user_id".into(), json!(user_id));
1603 }
1604 _ = output.insert(
1605 "client_id_issued_at".into(),
1606 json!(client.created_at.map(|time| time.timestamp())),
1607 );
1608 if let Some(secret) = plaintext {
1609 _ = output.insert(
1610 "client_secret".into(),
1611 Value::String(self.prefixed_client_secret(secret)),
1612 );
1613 _ = output.insert("client_secret_expires_at".into(), json!(0));
1614 }
1615 Ok(EndpointResponse::json(&Value::Object(output))?.with_status(201))
1616 }
1617 pub fn register_oauth_client_endpoint(
1619 input: &impl Serialize,
1620 ) -> AuthResult<ServerEndpoint<Value>> {
1621 ServerEndpoint::new("oauth-provider", "registerOAuthClient").with_body(input)
1622 }
1623 pub(super) async fn register_client<S: AuthSchema>(
1624 &self,
1625 body: Option<&JsValue>,
1626 req: &impl SessionRequest,
1627 ctx: &AuthContext<S>,
1628 ) -> AuthResult<EndpointResponse> {
1629 let mut metadata = match registration_metadata(body)? {
1630 Ok(metadata) => metadata,
1631 Err(response) => return Ok(response),
1632 };
1633 req.queue_response_header("Cache-Control", "no-store");
1634 req.queue_response_header("Pragma", "no-cache");
1635 if !self.config.allow_dynamic_client_registration {
1636 return status_protocol_error(403, "access_denied", "Client registration is disabled");
1637 }
1638 let session = Box::pin(ctx.require_cached_session(req)).await.ok();
1639 if session.is_some()
1640 && let Some(response) = self
1641 .require_client_privileges("create", None, req, ctx)
1642 .await?
1643 {
1644 return Ok(response);
1645 }
1646 let mut token_authorization = None;
1647 if session.is_none()
1648 && let Some(authorization) = req.session_headers().get("authorization")
1649 {
1650 let mut parts = authorization
1651 .split(alibi_core::utils::javascript::is_whitespace)
1652 .filter(|part| !part.is_empty());
1653 if parts
1654 .next()
1655 .is_some_and(|scheme| scheme.eq_ignore_ascii_case("bearer"))
1656 {
1657 let token = parts.next();
1658 if token.is_none() || parts.next().is_some() {
1659 return status_protocol_error(
1660 400,
1661 "invalid_request",
1662 "Malformed initial access token Authorization header",
1663 )
1664 .map(|response| {
1665 response.with_header("WWW-Authenticate", "Bearer error=\"invalid_request\"")
1666 });
1667 }
1668 let mut client_metadata = metadata.clone();
1669 if let Some(resources) = body.and_then(|body| body.get("resources")) {
1670 _ = client_metadata.insert(
1671 "resources".into(),
1672 alibi_core::utils::json::from_value(resources.clone())?,
1673 );
1674 }
1675 let mut headers = alibi_core::Headers::new();
1676 for (name, value) in req.session_headers() {
1677 _ = headers.insert(name.clone(), value.clone());
1678 }
1679 token_authorization = Some(
1680 match self
1681 .authorize_initial_access_token(super::OAuthInitialAccessTokenInput {
1682 initial_access_token: token.unwrap_or_default(),
1683 headers: &headers,
1684 client_metadata: &client_metadata,
1685 })
1686 .await?
1687 {
1688 Ok(authorization) => authorization,
1689 Err(response) => return Ok(response),
1690 },
1691 );
1692 }
1693 }
1694 if session.is_none()
1695 && token_authorization.is_none()
1696 && !self.config.allow_unauthenticated_client_registration
1697 {
1698 return Ok(EndpointResponse::error(AuthError::Api {
1699 status: 401,
1700 code: None,
1701 message: String::new(),
1702 })
1703 .with_error_body(alibi_core::utils::json::parse_value(
1704 r#"{"error_description":"Authentication required for client registration"}"#,
1705 )?)
1706 .with_header("WWW-Authenticate", "Bearer"));
1707 }
1708 if let Some(requested) = metadata
1711 .get("grant_types")
1712 .and_then(Value::as_array)
1713 .filter(|grants| !grants.is_empty())
1714 .cloned()
1715 {
1716 let supported = self.supported_grant_types();
1717 let mutual = requested
1718 .iter()
1719 .filter(|grant| {
1720 grant
1721 .as_str()
1722 .is_some_and(|grant| supported.iter().any(|value| value == grant))
1723 })
1724 .cloned()
1725 .collect::<Vec<_>>();
1726 if mutual.is_empty() {
1727 return status_protocol_error(
1728 400,
1729 "invalid_client_metadata",
1730 &format!(
1731 "no mutually supported grant_type among {}",
1732 requested
1733 .iter()
1734 .map(|grant| grant.as_str().unwrap_or_default())
1735 .collect::<Vec<_>>()
1736 .join(", ")
1737 ),
1738 );
1739 }
1740 let code = json!("authorization_code");
1743 if requested.contains(&code) && !mutual.contains(&code) {
1744 _ = metadata.remove("response_types");
1745 }
1746 _ = metadata.insert("grant_types".into(), Value::Array(mutual));
1747 }
1748 if session.is_none()
1749 && token_authorization.is_none()
1750 && metadata
1751 .get("grant_types")
1752 .and_then(Value::as_array)
1753 .is_some_and(|grants| grants.contains(&json!("client_credentials")))
1754 {
1755 return status_protocol_error(
1756 400,
1757 "invalid_client_metadata",
1758 "client_credentials grant requires authenticated registration",
1759 );
1760 }
1761 if metadata
1762 .get("scope")
1763 .and_then(Value::as_str)
1764 .is_none_or(str::is_empty)
1765 {
1766 _ = metadata.insert(
1767 "scope".into(),
1768 json!(
1769 self.config
1770 .client_registration_default_scopes
1771 .as_ref()
1772 .unwrap_or(&self.config.scopes)
1773 .join(" ")
1774 ),
1775 );
1776 }
1777 let registration_scopes = super::unique(
1778 self.config
1779 .client_registration_default_scopes
1780 .as_ref()
1781 .unwrap_or(&self.config.scopes)
1782 .iter()
1783 .chain(
1784 self.config
1785 .client_registration_allowed_scopes
1786 .iter()
1787 .flatten(),
1788 )
1789 .cloned()
1790 .collect(),
1791 );
1792 if !metadata.contains_key("application_type") {
1793 let application = self
1794 .config
1795 .client_registration_default_application_type
1796 .resolve(metadata.get("redirect_uris"));
1797 if application != "web" {
1798 _ = metadata.insert("application_type".into(), json!(application));
1799 }
1800 }
1801 let mut metadata = match self.admit_metadata(metadata, ®istration_scopes, ctx)? {
1802 Ok(metadata) => metadata,
1803 Err(response) => return Ok(response),
1804 };
1805 if let Some(resources) = body.and_then(|body| body.get("resources")) {
1806 _ = metadata.insert(
1807 "resources".into(),
1808 alibi_core::utils::json::from_value(resources.clone())?,
1809 );
1810 }
1811 _ = metadata.insert("scope".into(), json!(registration_scopes.join(" ")));
1812 if self.config.client_registration_require_pkce == Some(false)
1813 && metadata
1814 .get("token_endpoint_auth_method")
1815 .and_then(Value::as_str)
1816 != Some("none")
1817 {
1818 _ = metadata
1819 .entry("require_pkce")
1820 .or_insert_with(|| json!(false));
1821 }
1822 if let Some(reference) =
1823 token_authorization.and_then(|authorization| authorization.reference_id)
1824 {
1825 _ = metadata.insert("reference_id".into(), json!(reference));
1826 }
1827 let user_id = session.map(|(user, _)| user.id().as_ref().to_owned());
1828 self.persist_client(metadata, user_id, true, ctx).await
1829 }
1830}
1831fn registration_metadata(
1832 body: Option<&JsValue>,
1833) -> AuthResult<Result<serde_json::Map<String, Value>, EndpointResponse>> {
1834 let validated = match validate(body) {
1835 Ok(value) => value,
1836 Err(error) => {
1837 let message = error.error_payload().2;
1838 let first = message.split("; ").next().unwrap_or(&message);
1839 let (path, issue) = first
1840 .strip_prefix("[body.")
1841 .and_then(|text| text.split_once("] "))
1842 .unwrap_or_else(|| ("", first.trim_start_matches("[body] ")));
1843 let error = if path.starts_with("redirect_uris")
1844 || path.starts_with("post_logout_redirect_uris")
1845 {
1846 "invalid_redirect_uri"
1847 } else if path == "software_statement" {
1848 "invalid_software_statement"
1849 } else {
1850 "invalid_client_metadata"
1851 };
1852 let description = if path.is_empty() {
1853 issue.to_owned()
1854 } else if let Some(expected) = issue
1855 .strip_prefix("Invalid input: expected ")
1856 .and_then(|text| text.split_once(','))
1857 {
1858 format!("{path} must be a {}", expected.0)
1859 } else {
1860 format!("{path}: {issue}")
1861 };
1862 return status_protocol_error(400, error, &description).map(Err);
1863 }
1864 };
1865 let mut metadata: serde_json::Map<String, Value> =
1866 alibi_core::utils::json::from_value(validated)?;
1867 if metadata
1868 .get("client_name")
1869 .and_then(Value::as_str)
1870 .is_some_and(|name| js_trim(name).is_empty())
1871 {
1872 return status_protocol_error(
1873 400,
1874 "invalid_client_metadata",
1875 "client_name: client_name cannot be empty",
1876 )
1877 .map(Err);
1878 }
1879 if let Some(subject) = body.and_then(|body| body.get("subject_type")) {
1880 if !subject
1881 .as_str()
1882 .is_some_and(|value| ["public", "pairwise"].contains(&value))
1883 {
1884 return status_protocol_error(
1885 400,
1886 "invalid_client_metadata",
1887 "subject_type must be one of: public, pairwise",
1888 )
1889 .map(Err);
1890 }
1891 _ = metadata.insert("subject_type".into(), json!(subject.as_str()));
1892 }
1893 if let Some(value) = body.and_then(|body| body.get("resources")) {
1894 let Some(resources) = value.as_array() else {
1895 return status_protocol_error(400, "invalid_target", "resources must be a array")
1896 .map(Err);
1897 };
1898 for (index, value) in resources.iter().enumerate() {
1899 let Some(uri) = value.as_str() else {
1900 return status_protocol_error(
1901 400,
1902 "invalid_target",
1903 &format!("resources[{index}] must be a string"),
1904 )
1905 .map(Err);
1906 };
1907 let error = super::resources::resource_uri_issue(uri);
1908 if let Some(error) = error {
1909 return status_protocol_error(
1910 400,
1911 "invalid_target",
1912 &format!("resources[{index}]: {error}"),
1913 )
1914 .map(Err);
1915 }
1916 }
1917 }
1918 if body.and_then(|body| body.get("skip_consent")).is_some() {
1919 return status_protocol_error(
1920 400,
1921 "invalid_client_metadata",
1922 "skip_consent must be a never",
1923 )
1924 .map(Err);
1925 }
1926 Ok(Ok(metadata))
1927}
1928
1929pub(super) fn validate_admin_create(body: Option<&JsValue>) -> AuthResult<JsValue> {
1930 let mut fields = Vec::new();
1931 for &(name, kind) in FIELDS {
1932 if name == "dpop_bound_access_tokens" {
1933 continue;
1934 }
1935 if name == "response_types" {
1936 fields.push(("client_credentials_scopes", FieldKind::Scopes));
1937 fields.push(("resources", FieldKind::Resources));
1938 }
1939 fields.push((name, kind));
1940 }
1941 fields.extend([
1942 ("client_secret_expires_at", FieldKind::Expiration),
1943 ("skip_consent", FieldKind::Boolean),
1944 ("enable_end_session", FieldKind::Boolean),
1945 ("require_pkce", FieldKind::Boolean),
1946 ("dpop_bound_access_tokens", FieldKind::Boolean),
1947 ("subject_type", FieldKind::Subject),
1948 ("metadata", FieldKind::Record),
1949 ]);
1950 validate_metadata(body, "body", &fields)
1951}
1952impl OAuthProviderPlugin {
1953 pub fn admin_create_oauth_client_endpoint(
1955 input: &impl Serialize,
1956 ) -> AuthResult<ServerEndpoint<Value>> {
1957 ServerEndpoint::new("oauth-provider", "adminCreateOAuthClient").with_body(input)
1958 }
1959 pub(super) async fn admin_create_client<S: AuthSchema>(
1960 &self,
1961 mut metadata: serde_json::Map<String, Value>,
1962 req: &impl SessionRequest,
1963 ctx: &AuthContext<S>,
1964 ) -> AuthResult<EndpointResponse> {
1965 req.queue_response_header("Cache-Control", "no-store");
1966 req.queue_response_header("Pragma", "no-cache");
1967 let Some((user, _)) = Box::pin(ctx.require_cached_session(req)).await.ok() else {
1968 return Ok(EndpointResponse::error(AuthError::Api {
1969 status: 401,
1970 code: None,
1971 message: String::new(),
1972 })
1973 .without_error_body()
1974 .with_header("Content-Type", "application/json"));
1975 };
1976 if let Some(response) = self
1977 .require_client_privileges("create", None, req, ctx)
1978 .await?
1979 {
1980 return Ok(response);
1981 }
1982 let scopes = metadata
1983 .remove("client_credentials_scopes")
1984 .and_then(|value| value.as_array().cloned())
1985 .unwrap_or_default();
1986 if let Some(response) = self
1987 .admit_credential_scopes(&scopes, &metadata, None, req, ctx, true)
1988 .await?
1989 {
1990 return Ok(response);
1991 }
1992 let resources = metadata.remove("resources");
1994 let mut linked = Vec::new();
1995 for identifier in resources
1996 .as_ref()
1997 .and_then(Value::as_array)
1998 .into_iter()
1999 .flatten()
2000 .filter_map(Value::as_str)
2001 {
2002 if linked.contains(&identifier) {
2003 continue;
2004 }
2005 linked.push(identifier);
2006 if let Some(response) =
2007 Box::pin(self.resource_admission(req, ctx, "link", Some(identifier))).await?
2008 {
2009 return Ok(response);
2010 }
2011 }
2012 _ = metadata.remove("client_secret_expires_at");
2013 let extensions = metadata
2014 .remove("metadata")
2015 .and_then(|value| value.as_object().cloned())
2016 .unwrap_or_default();
2017 let mut metadata = match self.admit_metadata(metadata, &self.config.scopes, ctx)? {
2018 Ok(metadata) => metadata,
2019 Err(response) => return Ok(response),
2020 };
2021 let extensions = extensions
2022 .into_iter()
2023 .filter(|(name, _)| !RESERVED_METADATA_FIELDS.contains(&name.as_str()))
2024 .collect::<serde_json::Map<_, _>>();
2025 if !extensions.is_empty() {
2026 _ = metadata.insert("metadata".into(), Value::Object(extensions));
2027 }
2028 _ = metadata.insert("client_credentials_scopes".into(), json!(scopes));
2029 if let Some(resources) = resources {
2030 _ = metadata.insert("resources".into(), resources);
2031 }
2032 self.persist_client(metadata, Some(user.id().as_ref().to_owned()), false, ctx)
2033 .await
2034 }
2035}
2036
2037pub(super) const RESERVED_METADATA_FIELDS: &[&str] = &[
2038 "clientId",
2039 "clientSecret",
2040 "clientDiscoveryId",
2041 "disabled",
2042 "scopes",
2043 "clientCredentialsScopes",
2044 "userId",
2045 "createdAt",
2046 "updatedAt",
2047 "expiresAt",
2048 "name",
2049 "uri",
2050 "icon",
2051 "contacts",
2052 "tos",
2053 "policy",
2054 "softwareId",
2055 "softwareVersion",
2056 "softwareStatement",
2057 "redirectUris",
2058 "postLogoutRedirectUris",
2059 "backchannelLogoutUri",
2060 "backchannelLogoutSessionRequired",
2061 "tokenEndpointAuthMethod",
2062 "grantTypes",
2063 "responseTypes",
2064 "applicationType",
2065 "jwks",
2066 "jwksUri",
2067 "requirePKCE",
2068 "dpopBoundAccessTokens",
2069 "skipConsent",
2070 "enableEndSession",
2071 "subjectType",
2072 "referenceId",
2073 "metadata",
2074 "client_id",
2075 "client_secret",
2076 "client_secret_expires_at",
2077 "scope",
2078 "user_id",
2079 "client_id_issued_at",
2080 "client_name",
2081 "client_uri",
2082 "logo_uri",
2083 "tos_uri",
2084 "policy_uri",
2085 "jwks_uri",
2086 "software_id",
2087 "software_version",
2088 "software_statement",
2089 "redirect_uris",
2090 "post_logout_redirect_uris",
2091 "backchannel_logout_uri",
2092 "backchannel_logout_session_required",
2093 "token_endpoint_auth_method",
2094 "grant_types",
2095 "response_types",
2096 "application_type",
2097 "skip_consent",
2098 "enable_end_session",
2099 "require_pkce",
2100 "dpop_bound_access_tokens",
2101 "subject_type",
2102 "reference_id",
2103 "public",
2104 "type",
2105 "resources",
2106 "client_credentials_scopes",
2107];
2108
2109impl OAuthProviderPlugin {
2110 async fn admit_credential_scopes<S: AuthSchema>(
2111 &self,
2112 scopes: &[Value],
2113 metadata: &serde_json::Map<String, Value>,
2114 client_id: Option<&str>,
2115 req: &impl SessionRequest,
2116 ctx: &AuthContext<S>,
2117 require_privileges: bool,
2118 ) -> AuthResult<Option<EndpointResponse>> {
2119 if !scopes.is_empty() {
2120 if !metadata
2121 .get("grant_types")
2122 .and_then(Value::as_array)
2123 .is_some_and(|grants| grants.contains(&json!("client_credentials")))
2124 {
2125 return status_protocol_error(
2126 400,
2127 "invalid_client_metadata",
2128 "client_credentials_scopes requires the client_credentials grant",
2129 )
2130 .map(Some);
2131 }
2132 if metadata
2133 .get("token_endpoint_auth_method")
2134 .and_then(Value::as_str)
2135 == Some("none")
2136 {
2137 return status_protocol_error(
2138 400,
2139 "invalid_client_metadata",
2140 "public clients cannot be assigned client_credentials scopes",
2141 )
2142 .map(Some);
2143 }
2144 let invalid = scopes
2145 .iter()
2146 .filter_map(Value::as_str)
2147 .filter(|scope| {
2148 !self.config.scopes.iter().any(|allowed| allowed == scope)
2149 || ["openid", "profile", "email", "offline_access"].contains(scope)
2150 })
2151 .collect::<Vec<_>>();
2152 if !invalid.is_empty() {
2153 return status_protocol_error(
2154 400,
2155 "invalid_scope",
2156 &format!(
2157 "The following client_credentials scopes are invalid: {}",
2158 invalid.join(", ")
2159 ),
2160 )
2161 .map(Some);
2162 }
2163 if require_privileges {
2164 return self
2165 .require_client_privileges(
2166 "configure-client-credentials-scopes",
2167 client_id,
2168 req,
2169 ctx,
2170 )
2171 .await;
2172 }
2173 }
2174 Ok(None)
2175 }
2176}
2177
2178fn client_privilege_denied(status: u16) -> EndpointResponse {
2179 EndpointResponse::error(AuthError::Api {
2180 status,
2181 code: None,
2182 message: String::new(),
2183 })
2184 .without_error_body()
2185 .with_header("Content-Type", "application/json")
2186}
2187
2188fn client_update_response(
2189 client: alibi_core::store::OAuthClient,
2190 admin: bool,
2191) -> AuthResult<EndpointResponse> {
2192 let scopes = client
2193 .metadata
2194 .get("client_credentials_scopes")
2195 .cloned()
2196 .unwrap_or_else(|| json!([]));
2197 let mut output = client_read_metadata(client)?;
2198 if admin {
2199 let object = output
2200 .as_object_mut()
2201 .ok_or_else(|| AuthError::internal("OAuth client metadata must be an object"))?;
2202 _ = object.insert("client_credentials_scopes".into(), scopes);
2203 }
2204 EndpointResponse::json(&output)
2205}
2206pub(super) fn validate_admin_update(body: Option<&JsValue>) -> AuthResult<JsValue> {
2207 let mut output = crate::endpoint::validate_fields(
2208 body,
2209 "body",
2210 &[crate::authentication_helpers::JsonField::string(
2211 "client_id",
2212 true,
2213 )],
2214 )?;
2215 let mut fields = FIELDS
2216 .iter()
2217 .copied()
2218 .filter(|(name, _)| {
2219 ![
2220 "token_endpoint_auth_method",
2221 "jwks",
2222 "jwks_uri",
2223 "dpop_bound_access_tokens",
2224 ]
2225 .contains(name)
2226 })
2227 .collect::<Vec<_>>();
2228 let position = fields
2229 .iter()
2230 .position(|(name, _)| *name == "response_types")
2231 .unwrap_or(fields.len());
2232 fields.insert(position, ("client_credentials_scopes", FieldKind::Scopes));
2233 fields.extend([
2234 ("client_secret_expires_at", FieldKind::Expiration),
2235 ("skip_consent", FieldKind::Boolean),
2236 ("enable_end_session", FieldKind::Boolean),
2237 ("disabled", FieldKind::Boolean),
2238 ("dpop_bound_access_tokens", FieldKind::Boolean),
2239 ("metadata", FieldKind::Record),
2240 ]);
2241 let update = validate_metadata(
2242 body.and_then(|body| body.get("update")),
2243 "body.update",
2244 &fields,
2245 )?;
2246 if let JsValue::Object(object) = &mut output {
2247 _ = object.insert("update".into(), update);
2248 }
2249 Ok(output)
2250}
2251impl OAuthProviderPlugin {
2252 pub fn admin_update_oauth_client_endpoint(
2254 client_id: &str,
2255 update: &impl Serialize,
2256 ) -> AuthResult<ServerEndpoint<Value>> {
2257 ServerEndpoint::new("oauth-provider", "adminUpdateOAuthClient")
2258 .with_body(&json!({"client_id":client_id,"update":update}))
2259 }
2260}
2261
2262#[derive(serde::Deserialize)]
2263pub(super) struct PreloginClientInput {
2264 client_id: String,
2265 oauth_query: Option<String>,
2266}
2267pub(super) fn validate_prelogin(body: Option<&JsValue>) -> AuthResult<JsValue> {
2268 crate::endpoint::validate_fields(
2269 body,
2270 "body",
2271 &[
2272 crate::authentication_helpers::JsonField::string("client_id", true),
2273 crate::authentication_helpers::JsonField::string("oauth_query", false),
2274 ],
2275 )
2276}
2277impl OAuthProviderPlugin {
2278 pub fn get_oauth_client_public_prelogin_endpoint(
2280 client_id: &str,
2281 oauth_query: Option<&str>,
2282 ) -> AuthResult<ServerEndpoint<Value>> {
2283 let mut input = serde_json::Map::from_iter([("client_id".into(), json!(client_id))]);
2284 if let Some(query) = oauth_query {
2285 _ = input.insert("oauth_query".into(), json!(query));
2286 }
2287 ServerEndpoint::new("oauth-provider", "getOAuthClientPublicPrelogin").with_body(&input)
2288 }
2289 pub(super) async fn read_prelogin_client<S: AuthSchema>(
2290 &self,
2291 input: PreloginClientInput,
2292 ctx: &AuthContext<S>,
2293 ) -> AuthResult<EndpointResponse> {
2294 let valid = super::authorization::verify_authorization_query(
2295 input.oauth_query.as_deref().unwrap_or(""),
2296 ctx.config.current_secret(),
2297 )?;
2298 if input
2300 .oauth_query
2301 .as_deref()
2302 .is_some_and(|query| !query.is_empty())
2303 && !valid
2304 {
2305 return Ok(EndpointResponse::error(AuthError::Api {
2306 status: 400,
2307 code: None,
2308 message: String::new(),
2309 })
2310 .with_error_body(alibi_core::utils::json::parse_value(
2311 r#"{"error":"invalid_signature"}"#,
2312 )?));
2313 }
2314 if !self.config.allow_public_client_prelogin {
2315 return Ok(EndpointResponse::error(AuthError::Api {
2316 status: 400,
2317 code: None,
2318 message: String::new(),
2319 })
2320 .without_error_body()
2321 .with_header("Content-Type", "application/json"));
2322 }
2323 if !valid {
2324 return Ok(EndpointResponse::error(AuthError::Api {
2325 status: 401,
2326 code: None,
2327 message: String::new(),
2328 })
2329 .with_error_body(alibi_core::utils::json::parse_value(
2330 r#"{"error":"invalid_signature"}"#,
2331 )?));
2332 }
2333 Box::pin(self.public_client_projection(&input.client_id, ctx)).await
2334 }
2335}
2336
2337fn validate_rotate_keys(
2341 body: Option<&JsValue>,
2342) -> AuthResult<Result<(String, String, Value), EndpointResponse>> {
2343 let field = |name: &str| body.and_then(|body| body.get(name));
2344 for name in ["client_assertion", "client_assertion_type"] {
2345 let description = match field(name) {
2346 Some(JsValue::String(_)) => continue,
2347 None => format!("{name} is required"),
2348 Some(JsValue::Array(_)) => format!("{name} must not appear more than once"),
2349 Some(_) => format!("{name} must be a string"),
2350 };
2351 return status_protocol_error(400, "invalid_client", &description).map(Err);
2352 }
2353 if field("jwks").is_none() {
2354 return status_protocol_error(400, "invalid_client_metadata", "jwks is required").map(Err);
2355 }
2356 if let Err(error) = validate_metadata(body, "body", &[("jwks", FieldKind::Jwks)]) {
2357 let message = error.error_payload().2;
2358 let first = message.split("; ").next().unwrap_or(&message);
2359 let (path, issue) = first
2360 .strip_prefix("[body.")
2361 .and_then(|text| text.split_once("] "))
2362 .unwrap_or(("jwks", first));
2363 let description = if let Some(expected) = issue
2364 .strip_prefix("Invalid input: expected ")
2365 .and_then(|text| text.split_once(','))
2366 {
2367 if expected.1.trim() == "received array" {
2368 format!("{path} must not appear more than once")
2369 } else {
2370 format!("{path} must be a {}", expected.0)
2371 }
2372 } else {
2373 format!("{path}: {issue}")
2374 };
2375 return status_protocol_error(400, "invalid_client_metadata", &description).map(Err);
2376 }
2377 let text = |name: &str| {
2378 field(name)
2379 .and_then(JsValue::as_str)
2380 .unwrap_or_default()
2381 .to_owned()
2382 };
2383 let jwks = field("jwks")
2384 .map(JsValue::to_json_value)
2385 .transpose()?
2386 .unwrap_or(Value::Null);
2387 Ok(Ok((
2388 text("client_assertion"),
2389 text("client_assertion_type"),
2390 jwks,
2391 )))
2392}
2393fn rotatable(
2395 provider: &OAuthProviderPlugin,
2396 client: &alibi_core::store::OAuthClient,
2397) -> Option<(u16, &'static str)> {
2398 if provider
2399 .config
2400 .cached_trusted_clients
2401 .contains(&client.client_id)
2402 {
2403 return Some((400, "trusted clients must be updated manually"));
2404 }
2405 let present = |name: &str| {
2406 client
2407 .metadata
2408 .get(name)
2409 .is_some_and(|value| !value.is_null())
2410 };
2411 (present("client_discovery_id") || present("jwks_uri") || !present("jwks")).then_some((
2412 400,
2413 "client keys are not stored inline; rotate them where the client publishes them",
2414 ))
2415}
2416impl OAuthProviderPlugin {
2417 pub fn rotate_client_keys_endpoint(
2420 input: &impl Serialize,
2421 ) -> AuthResult<ServerEndpoint<Value>> {
2422 ServerEndpoint::new("oauth-provider", "rotateClientKeys").with_body(input)
2423 }
2424 pub(super) async fn rotate_client_keys<S: AuthSchema>(
2425 &self,
2426 body: Option<&JsValue>,
2427 req: &impl SessionRequest,
2428 ctx: &AuthContext<S>,
2429 ) -> AuthResult<EndpointResponse> {
2430 req.queue_response_header("Cache-Control", "no-store");
2431 req.queue_response_header("Pragma", "no-cache");
2432 let (assertion, assertion_type, jwks) = match validate_rotate_keys(body)? {
2433 Ok(input) => input,
2434 Err(response) => return Ok(response),
2435 };
2436 if !self.config.allow_client_key_rotation {
2437 return status_protocol_error(403, "access_denied", "Client key rotation is disabled");
2438 }
2439 if !jwks
2440 .get("keys")
2441 .and_then(Value::as_array)
2442 .is_some_and(|keys| !keys.is_empty() && keys.iter().all(Value::is_object))
2443 {
2444 return status_protocol_error(
2445 400,
2446 "invalid_client_metadata",
2447 "jwks must be an RFC 7517 JWK Set object with a non-empty keys array",
2448 );
2449 }
2450 if let Some(error) = public_jwks_error(&jwks) {
2451 return status_protocol_error(400, "invalid_client_metadata", error);
2452 }
2453 let base = format!(
2456 "{}{}",
2457 ctx.config.base_url.trim_end_matches('/'),
2458 ctx.config.base_path
2459 );
2460 let audience = format!("{}/oauth2/client/rotate-keys", base.trim_end_matches('/'));
2461 let credentials = json!({
2462 "client_assertion": assertion,
2463 "client_assertion_type": assertion_type,
2464 });
2465 let (_, client) = match Box::pin(super::assertion::verify_client(
2466 &credentials,
2467 ctx,
2468 self,
2469 &audience,
2470 true,
2471 Some(rotatable),
2472 ))
2473 .await?
2474 {
2475 Ok(verified) => verified,
2476 Err(response) => return Ok(response),
2477 };
2478 let expected = alibi_core::store::oauth_clients::metadata_to_columns(
2481 json!({"jwks": client.metadata.get("jwks")}),
2482 )?
2483 .get("jwks")
2484 .and_then(Value::as_str)
2485 .unwrap_or_default()
2486 .to_owned();
2487 let now = chrono::Utc::now();
2488 if ctx
2489 .database
2490 .replace_oauth_client_jwks(
2491 &client.client_id,
2492 &expected,
2493 serde_json::to_string(&jwks)?,
2494 now,
2495 )
2496 .await?
2497 .is_none()
2498 {
2499 return status_protocol_error(
2500 409,
2501 "invalid_request",
2502 "client keys changed while rotating; retry with a fresh client assertion",
2503 );
2504 }
2505 EndpointResponse::json(&json!({"client_id": client.client_id, "jwks": jwks}))
2506 }
2507}