1use std::collections::BTreeMap;
28
29use axum::extract::{Path, State};
30use axum::http::{HeaderMap, StatusCode};
31use axum::response::{IntoResponse, Json, Response};
32use serde::{Deserialize, Serialize};
33use serde_json::json;
34
35use crate::ctx::AuthCtx;
36use crate::state::AdminApiKeys;
37
38use super::auth_params;
39use super::issuer_validation;
40use super::types::{OidcClient, TokenAuthMethod, UpstreamProvider};
41
42pub(crate) async fn require_admin(
48 headers: &HeaderMap,
49 ctx: &AuthCtx,
50 keys: &AdminApiKeys,
51) -> Result<crate::gate::Caller, Box<Response>> {
52 crate::gate::require_role_for(headers, ctx, keys, "auth", "system", "admin").await
53}
54
55#[derive(Clone, Debug, Deserialize)]
63pub struct CreateClientBody {
64 pub client_id: Option<String>,
65 pub redirect_uris: Vec<String>,
66 pub name: String,
67 pub logo_url: Option<String>,
68 #[serde(default = "default_auth_method")]
69 pub token_endpoint_auth_method: String,
70 #[serde(default = "default_scopes")]
71 pub default_scopes: Vec<String>,
72 #[serde(default = "default_true")]
73 pub require_consent: bool,
74 #[serde(default = "default_grant_types")]
75 pub grant_types: Vec<String>,
76 #[serde(default = "default_response_types")]
77 pub response_types: Vec<String>,
78 #[serde(default = "default_true")]
79 pub pkce_required: bool,
80 pub backchannel_logout_uri: Option<String>,
81}
82
83fn default_auth_method() -> String {
84 "client_secret_basic".to_string()
85}
86fn default_scopes() -> Vec<String> {
87 vec!["openid".to_string()]
88}
89fn default_true() -> bool {
90 true
91}
92fn default_grant_types() -> Vec<String> {
93 vec![
94 "authorization_code".to_string(),
95 "refresh_token".to_string(),
96 ]
97}
98fn default_response_types() -> Vec<String> {
99 vec!["code".to_string()]
100}
101
102#[derive(Clone, Debug, Serialize)]
105pub struct CreateClientResponse {
106 pub client: OidcClient,
107 pub client_secret: Option<String>,
110}
111
112pub async fn create_client(
113 State(ctx): State<AuthCtx>,
114 State(keys): State<AdminApiKeys>,
115 headers: HeaderMap,
116 Json(body): Json<CreateClientBody>,
117) -> Response {
118 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
119 return *r;
120 }
121 let provider = match ctx.oidc_provider.as_ref() {
122 Some(p) => p,
123 None => return svc_unavailable("oidc_provider not enabled"),
124 };
125 if body.redirect_uris.is_empty() {
126 return bad_request("redirect_uris must be non-empty");
127 }
128 for u in &body.redirect_uris {
129 if url::Url::parse(u).is_err() {
130 return bad_request(&format!("redirect_uri {u:?} is not a URL"));
131 }
132 }
133 let auth_method = match TokenAuthMethod::parse(&body.token_endpoint_auth_method) {
134 Some(m) => m,
135 None => {
136 return bad_request(&format!(
137 "unknown token_endpoint_auth_method {:?}",
138 body.token_endpoint_auth_method
139 ));
140 }
141 };
142 let client_id = body.client_id.clone().unwrap_or_else(|| {
143 format!(
144 "ocl_{}",
145 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<12>())
146 )
147 });
148 let plaintext_secret = match auth_method {
149 TokenAuthMethod::None => None,
150 _ => Some(format!(
151 "ocs_{}",
152 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<24>())
153 )),
154 };
155 let secret_hash = match &plaintext_secret {
156 Some(s) => {
157 let hasher = crate::password::PasswordHasher::default();
158 match hasher.hash(s) {
159 Ok(h) => Some(h),
160 Err(e) => return server_error(&format!("hash secret: {e}")),
161 }
162 }
163 None => None,
164 };
165 let client = OidcClient {
166 client_id: client_id.clone(),
167 client_secret_hash: secret_hash,
168 redirect_uris: body.redirect_uris,
169 name: body.name,
170 logo_url: body.logo_url,
171 token_endpoint_auth_method: auth_method,
172 default_scopes: body.default_scopes,
173 require_consent: body.require_consent,
174 grant_types: body.grant_types,
175 response_types: body.response_types,
176 pkce_required: body.pkce_required,
177 backchannel_logout_uri: body.backchannel_logout_uri,
178 created_at: now_secs(),
179 };
180 if let Err(e) = provider.clients.create(&client).await {
181 return server_error(&format!("persist client: {e}"));
182 }
183 (
184 StatusCode::CREATED,
185 Json(CreateClientResponse {
186 client,
187 client_secret: plaintext_secret,
188 }),
189 )
190 .into_response()
191}
192
193pub async fn list_clients(
194 State(ctx): State<AuthCtx>,
195 State(keys): State<AdminApiKeys>,
196 headers: HeaderMap,
197) -> Response {
198 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
199 return *r;
200 }
201 let provider = match ctx.oidc_provider.as_ref() {
202 Some(p) => p,
203 None => return svc_unavailable("oidc_provider not enabled"),
204 };
205 match provider.clients.list().await {
206 Ok(list) => (StatusCode::OK, Json(list)).into_response(),
207 Err(e) => server_error(&format!("list clients: {e}")),
208 }
209}
210
211pub async fn get_client(
212 State(ctx): State<AuthCtx>,
213 State(keys): State<AdminApiKeys>,
214 headers: HeaderMap,
215 Path(id): Path<String>,
216) -> Response {
217 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
218 return *r;
219 }
220 let provider = match ctx.oidc_provider.as_ref() {
221 Some(p) => p,
222 None => return svc_unavailable("oidc_provider not enabled"),
223 };
224 match provider.clients.get(&id).await {
225 Ok(Some(c)) => (StatusCode::OK, Json(c)).into_response(),
226 Ok(None) => (
227 StatusCode::NOT_FOUND,
228 Json(json!({"error": "unknown client_id"})),
229 )
230 .into_response(),
231 Err(e) => server_error(&format!("get client: {e}")),
232 }
233}
234
235#[derive(Clone, Debug, Deserialize)]
239pub struct UpdateClientBody {
240 pub redirect_uris: Vec<String>,
241 pub name: String,
242 pub logo_url: Option<String>,
243 pub token_endpoint_auth_method: String,
244 pub default_scopes: Vec<String>,
245 pub require_consent: bool,
246 pub grant_types: Vec<String>,
247 pub response_types: Vec<String>,
248 pub pkce_required: bool,
249 pub backchannel_logout_uri: Option<String>,
250}
251
252pub async fn update_client(
253 State(ctx): State<AuthCtx>,
254 State(keys): State<AdminApiKeys>,
255 headers: HeaderMap,
256 Path(id): Path<String>,
257 Json(body): Json<UpdateClientBody>,
258) -> Response {
259 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
260 return *r;
261 }
262 let provider = match ctx.oidc_provider.as_ref() {
263 Some(p) => p,
264 None => return svc_unavailable("oidc_provider not enabled"),
265 };
266 let existing = match provider.clients.get(&id).await {
267 Ok(Some(c)) => c,
268 Ok(None) => {
269 return (
270 StatusCode::NOT_FOUND,
271 Json(json!({"error": "unknown client_id"})),
272 )
273 .into_response();
274 }
275 Err(e) => return server_error(&format!("client lookup: {e}")),
276 };
277 let auth_method = match TokenAuthMethod::parse(&body.token_endpoint_auth_method) {
278 Some(m) => m,
279 None => {
280 return bad_request(&format!(
281 "unknown token_endpoint_auth_method {:?}",
282 body.token_endpoint_auth_method
283 ));
284 }
285 };
286 let updated = OidcClient {
287 client_id: existing.client_id,
288 client_secret_hash: existing.client_secret_hash,
289 redirect_uris: body.redirect_uris,
290 name: body.name,
291 logo_url: body.logo_url,
292 token_endpoint_auth_method: auth_method,
293 default_scopes: body.default_scopes,
294 require_consent: body.require_consent,
295 grant_types: body.grant_types,
296 response_types: body.response_types,
297 pkce_required: body.pkce_required,
298 backchannel_logout_uri: body.backchannel_logout_uri,
299 created_at: existing.created_at,
300 };
301 if let Err(e) = provider.clients.update(&updated).await {
302 return server_error(&format!("update client: {e}"));
303 }
304 (StatusCode::OK, Json(updated)).into_response()
305}
306
307pub async fn delete_client(
308 State(ctx): State<AuthCtx>,
309 State(keys): State<AdminApiKeys>,
310 headers: HeaderMap,
311 Path(id): Path<String>,
312) -> Response {
313 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
314 return *r;
315 }
316 let provider = match ctx.oidc_provider.as_ref() {
317 Some(p) => p,
318 None => return svc_unavailable("oidc_provider not enabled"),
319 };
320 match provider.clients.delete(&id).await {
321 Ok(true) => StatusCode::NO_CONTENT.into_response(),
322 Ok(false) => (
323 StatusCode::NOT_FOUND,
324 Json(json!({"error": "unknown client_id"})),
325 )
326 .into_response(),
327 Err(e) => server_error(&format!("delete client: {e}")),
328 }
329}
330
331#[derive(Clone, Debug, Serialize)]
334pub struct RotateSecretResponse {
335 pub client_id: String,
336 pub client_secret: String,
337}
338
339pub async fn rotate_client_secret(
340 State(ctx): State<AuthCtx>,
341 State(keys): State<AdminApiKeys>,
342 headers: HeaderMap,
343 Path(id): Path<String>,
344) -> Response {
345 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
346 return *r;
347 }
348 let provider = match ctx.oidc_provider.as_ref() {
349 Some(p) => p,
350 None => return svc_unavailable("oidc_provider not enabled"),
351 };
352 let plaintext = format!(
353 "ocs_{}",
354 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<24>())
355 );
356 let hasher = crate::password::PasswordHasher::default();
357 let hash = match hasher.hash(&plaintext) {
358 Ok(h) => h,
359 Err(e) => return server_error(&format!("hash secret: {e}")),
360 };
361 match provider.clients.rotate_secret_hash(&id, &hash).await {
362 Ok(true) => (
363 StatusCode::OK,
364 Json(RotateSecretResponse {
365 client_id: id,
366 client_secret: plaintext,
367 }),
368 )
369 .into_response(),
370 Ok(false) => (
371 StatusCode::NOT_FOUND,
372 Json(json!({"error": "unknown client_id"})),
373 )
374 .into_response(),
375 Err(e) => server_error(&format!("rotate secret: {e}")),
376 }
377}
378
379#[derive(Clone, Debug, Deserialize)]
385pub struct UpstreamBody {
386 pub slug: String,
387 pub issuer: String,
388 pub client_id: String,
389 pub client_secret: String,
390 pub display_name: String,
391 pub icon_url: Option<String>,
392 #[serde(default = "default_true")]
393 pub enabled: bool,
394 #[serde(default)]
395 pub scopes: Option<Vec<String>>,
396 #[serde(default)]
397 pub auth_params: Option<BTreeMap<String, String>>,
398}
399
400pub async fn upsert_upstream(
401 State(ctx): State<AuthCtx>,
402 State(keys): State<AdminApiKeys>,
403 headers: HeaderMap,
404 Json(body): Json<UpstreamBody>,
405) -> Response {
406 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
407 return *r;
408 }
409 let provider = match ctx.oidc_provider.as_ref() {
410 Some(p) => p,
411 None => return svc_unavailable("oidc_provider not enabled"),
412 };
413
414 if let Err(e) = issuer_validation::validate_issuer(&body.issuer, false) {
418 return bad_request(&format!("issuer rejected: {e}"));
419 }
420
421 let auth_params_map = body.auth_params.unwrap_or_default();
422 let mut errors: Vec<serde_json::Value> = Vec::new();
423 for (k, v) in &auth_params_map {
424 if let Err(e) = auth_params::validate_pair(k, v) {
425 errors.push(json!({"key": k, "error": format!("{e}")}));
426 }
427 }
428 if !errors.is_empty() {
429 return (
430 StatusCode::BAD_REQUEST,
431 Json(json!({
432 "error": "invalid_request",
433 "error_description": "auth_params validation failed",
434 "auth_params_errors": errors,
435 })),
436 )
437 .into_response();
438 }
439
440 let scopes_vec = match body.scopes {
441 Some(v) if !v.is_empty() => normalize_scopes(v),
442 _ => crate::oidc::DEFAULT_UPSTREAM_SCOPES
443 .iter()
444 .map(|s| s.to_string())
445 .collect(),
446 };
447
448 let row = UpstreamProvider {
449 slug: body.slug,
450 issuer: body.issuer,
451 client_id: body.client_id,
452 client_secret: body.client_secret,
453 display_name: body.display_name,
454 icon_url: body.icon_url,
455 enabled: body.enabled,
456 scopes: scopes_vec,
457 auth_params: auth_params_map,
458 };
459 if let Err(e) = provider.upstream.upsert(&row).await {
460 return server_error(&format!("upsert upstream: {e}"));
461 }
462 if let Some(registry) = &ctx.oidc {
463 let row_clone = row.clone();
464 let public_url = provider.public_url.clone();
465 let registry = registry.clone();
466 let slug = row.slug.clone();
467 let handle = tokio::spawn(async move {
468 super::sync_upstream_to_registry(®istry, &row_clone, &public_url).await;
469 });
470 tokio::spawn(async move {
471 if let Err(e) = handle.await {
472 tracing::error!(
473 slug = %slug,
474 panic = e.is_panic(),
475 cancelled = e.is_cancelled(),
476 "registry sync task did not complete cleanly: {e}"
477 );
478 }
479 });
480 }
481 (StatusCode::OK, Json(row)).into_response()
482}
483
484fn normalize_scopes(mut scopes: Vec<String>) -> Vec<String> {
487 if !scopes.iter().any(|s| s == "openid") {
488 scopes.insert(0, "openid".to_string());
489 }
490 scopes
491}
492
493pub async fn list_upstream(
494 State(ctx): State<AuthCtx>,
495 State(keys): State<AdminApiKeys>,
496 headers: HeaderMap,
497) -> Response {
498 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
499 return *r;
500 }
501 let provider = match ctx.oidc_provider.as_ref() {
502 Some(p) => p,
503 None => return svc_unavailable("oidc_provider not enabled"),
504 };
505 match provider.upstream.list().await {
506 Ok(list) => (StatusCode::OK, Json(list)).into_response(),
507 Err(e) => server_error(&format!("list upstream: {e}")),
508 }
509}
510
511pub async fn get_upstream(
512 State(ctx): State<AuthCtx>,
513 State(keys): State<AdminApiKeys>,
514 headers: HeaderMap,
515 Path(slug): Path<String>,
516) -> Response {
517 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
518 return *r;
519 }
520 let provider = match ctx.oidc_provider.as_ref() {
521 Some(p) => p,
522 None => return svc_unavailable("oidc_provider not enabled"),
523 };
524 match provider.upstream.get(&slug).await {
525 Ok(Some(u)) => (StatusCode::OK, Json(u)).into_response(),
526 Ok(None) => (
527 StatusCode::NOT_FOUND,
528 Json(json!({"error": "unknown slug"})),
529 )
530 .into_response(),
531 Err(e) => server_error(&format!("get upstream: {e}")),
532 }
533}
534
535pub async fn delete_upstream(
536 State(ctx): State<AuthCtx>,
537 State(keys): State<AdminApiKeys>,
538 headers: HeaderMap,
539 Path(slug): Path<String>,
540) -> Response {
541 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
542 return *r;
543 }
544 let provider = match ctx.oidc_provider.as_ref() {
545 Some(p) => p,
546 None => return svc_unavailable("oidc_provider not enabled"),
547 };
548 match provider.upstream.delete(&slug).await {
549 Ok(true) => {
550 if let Some(registry) = &ctx.oidc {
551 registry.remove(&slug);
552 }
553 StatusCode::NO_CONTENT.into_response()
554 }
555 Ok(false) => (
556 StatusCode::NOT_FOUND,
557 Json(json!({"error": "unknown slug"})),
558 )
559 .into_response(),
560 Err(e) => server_error(&format!("delete upstream: {e}")),
561 }
562}
563
564fn bad_request(msg: &str) -> Response {
569 (
570 StatusCode::BAD_REQUEST,
571 Json(json!({"error": "invalid_request", "error_description": msg})),
572 )
573 .into_response()
574}
575
576fn server_error(msg: &str) -> Response {
577 (
578 StatusCode::INTERNAL_SERVER_ERROR,
579 Json(json!({"error": "server_error", "error_description": msg})),
580 )
581 .into_response()
582}
583
584fn svc_unavailable(msg: &str) -> Response {
585 (
586 StatusCode::SERVICE_UNAVAILABLE,
587 Json(json!({"error": "service_unavailable", "error_description": msg})),
588 )
589 .into_response()
590}
591
592fn now_secs() -> f64 {
593 use std::time::{SystemTime, UNIX_EPOCH};
594 SystemTime::now()
595 .duration_since(UNIX_EPOCH)
596 .unwrap_or_default()
597 .as_secs_f64()
598}
599
600fn random_bytes<const N: usize>() -> [u8; N] {
601 use rand::RngCore;
602 let mut buf = [0u8; N];
603 rand::rng().fill_bytes(&mut buf);
604 buf
605}
606
607