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