1use axum::extract::{Path, State};
28use axum::http::{HeaderMap, StatusCode};
29use axum::response::{IntoResponse, Json, Response};
30use serde::{Deserialize, Serialize};
31use serde_json::json;
32
33use crate::ctx::AuthCtx;
34use crate::state::AdminApiKeys;
35
36use super::types::{OidcClient, TokenAuthMethod, UpstreamProvider};
37
38pub(crate) async fn require_admin(
44 headers: &HeaderMap,
45 ctx: &AuthCtx,
46 keys: &AdminApiKeys,
47) -> Result<crate::gate::Caller, Box<Response>> {
48 crate::gate::require_role_for(headers, ctx, keys, "auth", "system", "admin").await
49}
50
51#[derive(Clone, Debug, Deserialize)]
59pub struct CreateClientBody {
60 pub client_id: Option<String>,
61 pub redirect_uris: Vec<String>,
62 pub name: String,
63 pub logo_url: Option<String>,
64 #[serde(default = "default_auth_method")]
65 pub token_endpoint_auth_method: String,
66 #[serde(default = "default_scopes")]
67 pub default_scopes: Vec<String>,
68 #[serde(default = "default_true")]
69 pub require_consent: bool,
70 #[serde(default = "default_grant_types")]
71 pub grant_types: Vec<String>,
72 #[serde(default = "default_response_types")]
73 pub response_types: Vec<String>,
74 #[serde(default = "default_true")]
75 pub pkce_required: bool,
76 pub backchannel_logout_uri: Option<String>,
77}
78
79fn default_auth_method() -> String {
80 "client_secret_basic".to_string()
81}
82fn default_scopes() -> Vec<String> {
83 vec!["openid".to_string()]
84}
85fn default_true() -> bool {
86 true
87}
88fn default_grant_types() -> Vec<String> {
89 vec!["authorization_code".to_string(), "refresh_token".to_string()]
90}
91fn default_response_types() -> Vec<String> {
92 vec!["code".to_string()]
93}
94
95#[derive(Clone, Debug, Serialize)]
98pub struct CreateClientResponse {
99 pub client: OidcClient,
100 pub client_secret: Option<String>,
103}
104
105pub async fn create_client(
106 State(ctx): State<AuthCtx>,
107 State(keys): State<AdminApiKeys>,
108 headers: HeaderMap,
109 Json(body): Json<CreateClientBody>,
110) -> Response {
111 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
112 return *r;
113 }
114 let provider = match ctx.oidc_provider.as_ref() {
115 Some(p) => p,
116 None => return svc_unavailable("oidc_provider not enabled"),
117 };
118 if body.redirect_uris.is_empty() {
119 return bad_request("redirect_uris must be non-empty");
120 }
121 for u in &body.redirect_uris {
122 if url::Url::parse(u).is_err() {
123 return bad_request(&format!("redirect_uri {u:?} is not a URL"));
124 }
125 }
126 let auth_method = match TokenAuthMethod::parse(&body.token_endpoint_auth_method) {
127 Some(m) => m,
128 None => {
129 return bad_request(&format!(
130 "unknown token_endpoint_auth_method {:?}",
131 body.token_endpoint_auth_method
132 ));
133 }
134 };
135 let client_id = body.client_id.clone().unwrap_or_else(|| {
136 format!(
137 "ocl_{}",
138 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<12>())
139 )
140 });
141 let plaintext_secret = match auth_method {
142 TokenAuthMethod::None => None,
143 _ => Some(format!(
144 "ocs_{}",
145 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<24>())
146 )),
147 };
148 let secret_hash = match &plaintext_secret {
149 Some(s) => {
150 let hasher = crate::password::PasswordHasher::default();
151 match hasher.hash(s) {
152 Ok(h) => Some(h),
153 Err(e) => return server_error(&format!("hash secret: {e}")),
154 }
155 }
156 None => None,
157 };
158 let client = OidcClient {
159 client_id: client_id.clone(),
160 client_secret_hash: secret_hash,
161 redirect_uris: body.redirect_uris,
162 name: body.name,
163 logo_url: body.logo_url,
164 token_endpoint_auth_method: auth_method,
165 default_scopes: body.default_scopes,
166 require_consent: body.require_consent,
167 grant_types: body.grant_types,
168 response_types: body.response_types,
169 pkce_required: body.pkce_required,
170 backchannel_logout_uri: body.backchannel_logout_uri,
171 created_at: now_secs(),
172 };
173 if let Err(e) = provider.clients.create(&client).await {
174 return server_error(&format!("persist client: {e}"));
175 }
176 (
177 StatusCode::CREATED,
178 Json(CreateClientResponse {
179 client,
180 client_secret: plaintext_secret,
181 }),
182 )
183 .into_response()
184}
185
186pub async fn list_clients(
187 State(ctx): State<AuthCtx>,
188 State(keys): State<AdminApiKeys>,
189 headers: HeaderMap,
190) -> Response {
191 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
192 return *r;
193 }
194 let provider = match ctx.oidc_provider.as_ref() {
195 Some(p) => p,
196 None => return svc_unavailable("oidc_provider not enabled"),
197 };
198 match provider.clients.list().await {
199 Ok(list) => (StatusCode::OK, Json(list)).into_response(),
200 Err(e) => server_error(&format!("list clients: {e}")),
201 }
202}
203
204pub async fn get_client(
205 State(ctx): State<AuthCtx>,
206 State(keys): State<AdminApiKeys>,
207 headers: HeaderMap,
208 Path(id): Path<String>,
209) -> Response {
210 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
211 return *r;
212 }
213 let provider = match ctx.oidc_provider.as_ref() {
214 Some(p) => p,
215 None => return svc_unavailable("oidc_provider not enabled"),
216 };
217 match provider.clients.get(&id).await {
218 Ok(Some(c)) => (StatusCode::OK, Json(c)).into_response(),
219 Ok(None) => (
220 StatusCode::NOT_FOUND,
221 Json(json!({"error": "unknown client_id"})),
222 )
223 .into_response(),
224 Err(e) => server_error(&format!("get client: {e}")),
225 }
226}
227
228#[derive(Clone, Debug, Deserialize)]
232pub struct UpdateClientBody {
233 pub redirect_uris: Vec<String>,
234 pub name: String,
235 pub logo_url: Option<String>,
236 pub token_endpoint_auth_method: String,
237 pub default_scopes: Vec<String>,
238 pub require_consent: bool,
239 pub grant_types: Vec<String>,
240 pub response_types: Vec<String>,
241 pub pkce_required: bool,
242 pub backchannel_logout_uri: Option<String>,
243}
244
245pub async fn update_client(
246 State(ctx): State<AuthCtx>,
247 State(keys): State<AdminApiKeys>,
248 headers: HeaderMap,
249 Path(id): Path<String>,
250 Json(body): Json<UpdateClientBody>,
251) -> Response {
252 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
253 return *r;
254 }
255 let provider = match ctx.oidc_provider.as_ref() {
256 Some(p) => p,
257 None => return svc_unavailable("oidc_provider not enabled"),
258 };
259 let existing = match provider.clients.get(&id).await {
260 Ok(Some(c)) => c,
261 Ok(None) => {
262 return (
263 StatusCode::NOT_FOUND,
264 Json(json!({"error": "unknown client_id"})),
265 )
266 .into_response();
267 }
268 Err(e) => return server_error(&format!("client lookup: {e}")),
269 };
270 let auth_method = match TokenAuthMethod::parse(&body.token_endpoint_auth_method) {
271 Some(m) => m,
272 None => {
273 return bad_request(&format!(
274 "unknown token_endpoint_auth_method {:?}",
275 body.token_endpoint_auth_method
276 ));
277 }
278 };
279 let updated = OidcClient {
280 client_id: existing.client_id,
281 client_secret_hash: existing.client_secret_hash,
282 redirect_uris: body.redirect_uris,
283 name: body.name,
284 logo_url: body.logo_url,
285 token_endpoint_auth_method: auth_method,
286 default_scopes: body.default_scopes,
287 require_consent: body.require_consent,
288 grant_types: body.grant_types,
289 response_types: body.response_types,
290 pkce_required: body.pkce_required,
291 backchannel_logout_uri: body.backchannel_logout_uri,
292 created_at: existing.created_at,
293 };
294 if let Err(e) = provider.clients.update(&updated).await {
295 return server_error(&format!("update client: {e}"));
296 }
297 (StatusCode::OK, Json(updated)).into_response()
298}
299
300pub async fn delete_client(
301 State(ctx): State<AuthCtx>,
302 State(keys): State<AdminApiKeys>,
303 headers: HeaderMap,
304 Path(id): Path<String>,
305) -> Response {
306 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
307 return *r;
308 }
309 let provider = match ctx.oidc_provider.as_ref() {
310 Some(p) => p,
311 None => return svc_unavailable("oidc_provider not enabled"),
312 };
313 match provider.clients.delete(&id).await {
314 Ok(true) => StatusCode::NO_CONTENT.into_response(),
315 Ok(false) => (
316 StatusCode::NOT_FOUND,
317 Json(json!({"error": "unknown client_id"})),
318 )
319 .into_response(),
320 Err(e) => server_error(&format!("delete client: {e}")),
321 }
322}
323
324#[derive(Clone, Debug, Serialize)]
327pub struct RotateSecretResponse {
328 pub client_id: String,
329 pub client_secret: String,
330}
331
332pub async fn rotate_client_secret(
333 State(ctx): State<AuthCtx>,
334 State(keys): State<AdminApiKeys>,
335 headers: HeaderMap,
336 Path(id): Path<String>,
337) -> Response {
338 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
339 return *r;
340 }
341 let provider = match ctx.oidc_provider.as_ref() {
342 Some(p) => p,
343 None => return svc_unavailable("oidc_provider not enabled"),
344 };
345 let plaintext = format!(
346 "ocs_{}",
347 data_encoding::BASE64URL_NOPAD.encode(&random_bytes::<24>())
348 );
349 let hasher = crate::password::PasswordHasher::default();
350 let hash = match hasher.hash(&plaintext) {
351 Ok(h) => h,
352 Err(e) => return server_error(&format!("hash secret: {e}")),
353 };
354 match provider.clients.rotate_secret_hash(&id, &hash).await {
355 Ok(true) => (
356 StatusCode::OK,
357 Json(RotateSecretResponse {
358 client_id: id,
359 client_secret: plaintext,
360 }),
361 )
362 .into_response(),
363 Ok(false) => (
364 StatusCode::NOT_FOUND,
365 Json(json!({"error": "unknown client_id"})),
366 )
367 .into_response(),
368 Err(e) => server_error(&format!("rotate secret: {e}")),
369 }
370}
371
372#[derive(Clone, Debug, Deserialize)]
378pub struct UpstreamBody {
379 pub slug: String,
380 pub issuer: String,
381 pub client_id: String,
382 pub client_secret: String,
383 pub display_name: String,
384 pub icon_url: Option<String>,
385 #[serde(default = "default_true")]
386 pub enabled: bool,
387}
388
389pub async fn upsert_upstream(
390 State(ctx): State<AuthCtx>,
391 State(keys): State<AdminApiKeys>,
392 headers: HeaderMap,
393 Json(body): Json<UpstreamBody>,
394) -> Response {
395 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
396 return *r;
397 }
398 let provider = match ctx.oidc_provider.as_ref() {
399 Some(p) => p,
400 None => return svc_unavailable("oidc_provider not enabled"),
401 };
402 let row = UpstreamProvider {
403 slug: body.slug,
404 issuer: body.issuer,
405 client_id: body.client_id,
406 client_secret: body.client_secret,
407 display_name: body.display_name,
408 icon_url: body.icon_url,
409 enabled: body.enabled,
410 };
411 if let Err(e) = provider.upstream.upsert(&row).await {
412 return server_error(&format!("upsert upstream: {e}"));
413 }
414 (StatusCode::OK, Json(row)).into_response()
415}
416
417pub async fn list_upstream(
418 State(ctx): State<AuthCtx>,
419 State(keys): State<AdminApiKeys>,
420 headers: HeaderMap,
421) -> Response {
422 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
423 return *r;
424 }
425 let provider = match ctx.oidc_provider.as_ref() {
426 Some(p) => p,
427 None => return svc_unavailable("oidc_provider not enabled"),
428 };
429 match provider.upstream.list().await {
430 Ok(list) => (StatusCode::OK, Json(list)).into_response(),
431 Err(e) => server_error(&format!("list upstream: {e}")),
432 }
433}
434
435pub async fn get_upstream(
436 State(ctx): State<AuthCtx>,
437 State(keys): State<AdminApiKeys>,
438 headers: HeaderMap,
439 Path(slug): Path<String>,
440) -> Response {
441 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
442 return *r;
443 }
444 let provider = match ctx.oidc_provider.as_ref() {
445 Some(p) => p,
446 None => return svc_unavailable("oidc_provider not enabled"),
447 };
448 match provider.upstream.get(&slug).await {
449 Ok(Some(u)) => (StatusCode::OK, Json(u)).into_response(),
450 Ok(None) => (
451 StatusCode::NOT_FOUND,
452 Json(json!({"error": "unknown slug"})),
453 )
454 .into_response(),
455 Err(e) => server_error(&format!("get upstream: {e}")),
456 }
457}
458
459pub async fn delete_upstream(
460 State(ctx): State<AuthCtx>,
461 State(keys): State<AdminApiKeys>,
462 headers: HeaderMap,
463 Path(slug): Path<String>,
464) -> Response {
465 if let Err(r) = require_admin(&headers, &ctx, &keys).await {
466 return *r;
467 }
468 let provider = match ctx.oidc_provider.as_ref() {
469 Some(p) => p,
470 None => return svc_unavailable("oidc_provider not enabled"),
471 };
472 match provider.upstream.delete(&slug).await {
473 Ok(true) => StatusCode::NO_CONTENT.into_response(),
474 Ok(false) => (
475 StatusCode::NOT_FOUND,
476 Json(json!({"error": "unknown slug"})),
477 )
478 .into_response(),
479 Err(e) => server_error(&format!("delete upstream: {e}")),
480 }
481}
482
483fn bad_request(msg: &str) -> Response {
488 (
489 StatusCode::BAD_REQUEST,
490 Json(json!({"error": "invalid_request", "error_description": msg})),
491 )
492 .into_response()
493}
494
495fn server_error(msg: &str) -> Response {
496 (
497 StatusCode::INTERNAL_SERVER_ERROR,
498 Json(json!({"error": "server_error", "error_description": msg})),
499 )
500 .into_response()
501}
502
503fn svc_unavailable(msg: &str) -> Response {
504 (
505 StatusCode::SERVICE_UNAVAILABLE,
506 Json(json!({"error": "service_unavailable", "error_description": msg})),
507 )
508 .into_response()
509}
510
511fn now_secs() -> f64 {
512 use std::time::{SystemTime, UNIX_EPOCH};
513 SystemTime::now()
514 .duration_since(UNIX_EPOCH)
515 .unwrap_or_default()
516 .as_secs_f64()
517}
518
519fn random_bytes<const N: usize>() -> [u8; N] {
520 use rand::RngCore;
521 let mut buf = [0u8; N];
522 rand::rng().fill_bytes(&mut buf);
523 buf
524}
525
526