1use std::collections::{BTreeMap, BTreeSet};
2
3use super::errors::WebError;
4use super::middleware::KOpId;
5use super::ServerState;
6use crate::https::extractors::VerifiedClientInformation;
7use axum::{
8 body::Body,
9 extract::{Path, Query, State},
10 http::{
11 header::{
12 ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, LOCATION,
13 WWW_AUTHENTICATE,
14 },
15 HeaderValue, StatusCode,
16 },
17 middleware::from_fn,
18 response::{IntoResponse, Response},
19 routing::{get, post},
20 Extension, Form, Json, Router,
21};
22use axum_macros::debug_handler;
23use kanidm_proto::constants::uri::{
24 OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT, OAUTH2_AUTHORISE_REJECT,
25};
26use kanidm_proto::constants::APPLICATION_JSON;
27use kanidm_proto::oauth2::AuthorisationResponse;
28
29#[cfg(feature = "dev-oauth2-device-flow")]
30use kanidm_proto::oauth2::DeviceAuthorizationResponse;
31use kanidmd_lib::idm::oauth2::{
32 AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthoriseResponse,
33 ErrorResponse, Oauth2Error, TokenRevokeRequest,
34};
35use kanidmd_lib::prelude::f_eq;
36use kanidmd_lib::prelude::*;
37use kanidmd_lib::value::PartialValue;
38use serde::{Deserialize, Serialize};
39use serde_with::formats::CommaSeparator;
40use serde_with::{serde_as, StringWithSeparator};
41
42#[cfg(feature = "dev-oauth2-device-flow")]
43use uri::OAUTH2_AUTHORISE_DEVICE;
44use uri::{OAUTH2_TOKEN_ENDPOINT, OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
45
46pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
50 filter_all!(f_and!([
51 f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
52 f_eq(Attribute::Name, PartialValue::new_iname(rs_name))
53 ]))
54}
55
56#[utoipa::path(
57 get,
58 path = "/ui/images/oauth2/{rs_name}",
59 operation_id = "oauth2_image_get",
60 responses(
61 (status = 200, description = "Ok", body=&[u8]),
62 (status = 401, description = "Authorization required"),
63 (status = 403, description = "Not Authorized"),
64 ),
65 security(("token_jwt" = [])),
66 tag = "ui",
67)]
68pub(crate) async fn oauth2_image_get(
71 State(state): State<ServerState>,
72 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
73 Path(rs_name): Path<String>,
74) -> Response {
75 let rs_filter = oauth2_id(&rs_name);
76 let res = state
77 .qe_r_ref
78 .handle_oauth2_rs_image_get_image(client_auth_info, rs_filter)
79 .await;
80
81 match res {
82 Ok(Some(image)) => (
83 StatusCode::OK,
84 [(CONTENT_TYPE, image.filetype.as_content_type_str())],
85 image.contents,
86 )
87 .into_response(),
88 Ok(None) => {
89 warn!(?rs_name, "No image set for OAuth2 client");
90 (StatusCode::NOT_FOUND, "").into_response()
91 }
92 Err(err) => WebError::from(err).into_response(),
93 }
94}
95
96#[instrument(level = "debug", skip(state, kopid))]
158pub async fn oauth2_authorise_post(
159 State(state): State<ServerState>,
160 Extension(kopid): Extension<KOpId>,
161 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
162 Json(auth_req): Json<AuthorisationRequest>,
163) -> impl IntoResponse {
164 let mut res = oauth2_authorise(state, auth_req, kopid, client_auth_info)
165 .await
166 .into_response();
167 if res.status() == StatusCode::FOUND {
168 *res.status_mut() = StatusCode::OK;
170 }
171 res
172}
173
174#[instrument(level = "debug", skip(state, kopid))]
175pub async fn oauth2_authorise_get(
176 State(state): State<ServerState>,
177 Extension(kopid): Extension<KOpId>,
178 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
179 Query(auth_req): Query<AuthorisationRequest>,
180) -> impl IntoResponse {
181 oauth2_authorise(state, auth_req, kopid, client_auth_info).await
183}
184
185async fn oauth2_authorise(
186 state: ServerState,
187 auth_req: AuthorisationRequest,
188 kopid: KOpId,
189 client_auth_info: ClientAuthInfo,
190) -> impl IntoResponse {
191 let res: Result<AuthoriseResponse, Oauth2Error> = state
192 .qe_r_ref
193 .handle_oauth2_authorise(client_auth_info, auth_req, kopid.eventid)
194 .await;
195
196 match res {
197 Ok(AuthoriseResponse::ConsentRequested {
198 client_name,
199 scopes,
200 pii_scopes,
201 consent_token,
202 }) => {
203 #[allow(clippy::unwrap_used)]
207 let body = serde_json::to_string(&AuthorisationResponse::ConsentRequested {
208 client_name,
209 scopes,
210 pii_scopes,
211 consent_token,
212 })
213 .unwrap();
214 #[allow(clippy::unwrap_used)]
215 Response::builder()
216 .status(StatusCode::OK)
217 .body(body.into())
218 .unwrap()
219 }
220 Ok(AuthoriseResponse::Permitted(success)) => {
221 #[allow(clippy::unwrap_used)]
224 let body =
225 Body::from(serde_json::to_string(&AuthorisationResponse::Permitted).unwrap());
226 let redirect_uri = success.build_redirect_uri();
227
228 #[allow(clippy::unwrap_used)]
229 Response::builder()
230 .status(StatusCode::FOUND)
231 .header(
232 LOCATION,
233 HeaderValue::from_str(redirect_uri.as_str()).unwrap(),
234 )
235 .header(
237 ACCESS_CONTROL_ALLOW_ORIGIN,
238 HeaderValue::from_str(&redirect_uri.origin().ascii_serialization()).unwrap(),
239 )
240 .body(body)
241 .unwrap()
242 }
243 Ok(AuthoriseResponse::AuthenticationRequired { .. })
244 | Err(Oauth2Error::AuthenticationRequired) => {
245 #[allow(clippy::unwrap_used)]
247 Response::builder()
248 .status(StatusCode::UNAUTHORIZED)
249 .header(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"))
250 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
251 .body(Body::empty())
252 .unwrap()
253 }
254 Err(Oauth2Error::AccessDenied) => {
255 #[allow(clippy::expect_used)]
257 Response::builder()
258 .status(StatusCode::FORBIDDEN)
259 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
260 .body(Body::empty())
261 .expect("Failed to generate a forbidden response")
262 }
263 Err(e) => {
274 admin_error!(
275 "Unable to authorise - Error ID: {:?} error: {}",
276 kopid.eventid,
277 &e.to_string()
278 );
279 #[allow(clippy::expect_used)]
280 Response::builder()
281 .status(StatusCode::BAD_REQUEST)
282 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
283 .body(Body::empty())
284 .expect("Failed to generate a bad request response")
285 }
286 }
287}
288
289pub async fn oauth2_authorise_permit_post(
290 State(state): State<ServerState>,
291 Extension(kopid): Extension<KOpId>,
292 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
293 Json(consent_req): Json<String>,
294) -> impl IntoResponse {
295 let mut res = oauth2_authorise_permit(state, consent_req, kopid, client_auth_info)
296 .await
297 .into_response();
298 if res.status() == StatusCode::FOUND {
299 *res.status_mut() = StatusCode::OK;
301 }
302 res
303}
304
305#[derive(Serialize, Deserialize, Debug)]
306pub struct ConsentRequestData {
307 token: String,
308}
309
310pub async fn oauth2_authorise_permit_get(
311 State(state): State<ServerState>,
312 Query(token): Query<ConsentRequestData>,
313 Extension(kopid): Extension<KOpId>,
314 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
315) -> impl IntoResponse {
316 oauth2_authorise_permit(state, token.token, kopid, client_auth_info).await
318}
319
320async fn oauth2_authorise_permit(
321 state: ServerState,
322 consent_req: String,
323 kopid: KOpId,
324 client_auth_info: ClientAuthInfo,
325) -> impl IntoResponse {
326 let res = state
327 .qe_w_ref
328 .handle_oauth2_authorise_permit(client_auth_info, consent_req, kopid.eventid)
329 .await;
330
331 match res {
332 Ok(success) => {
333 let redirect_uri = success.build_redirect_uri();
336
337 #[allow(clippy::expect_used)]
338 Response::builder()
339 .status(StatusCode::FOUND)
340 .header(LOCATION, redirect_uri.as_str())
341 .header(
342 ACCESS_CONTROL_ALLOW_ORIGIN,
343 redirect_uri.origin().ascii_serialization(),
344 )
345 .body(Body::empty())
346 .expect("Failed to generate response")
347 }
348 Err(err) => {
349 match err {
350 OperationError::NotAuthenticated => {
351 WebError::from(err).response_with_access_control_origin_header()
352 }
353 _ => {
354 #[allow(clippy::expect_used)]
363 Response::builder()
364 .status(StatusCode::INTERNAL_SERVER_ERROR)
365 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
366 .body(Body::empty())
367 .expect("Failed to generate error response")
368 }
369 }
370 }
371 }
372}
373
374pub async fn oauth2_authorise_reject_post(
376 State(state): State<ServerState>,
377 Extension(kopid): Extension<KOpId>,
378 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
379 Form(consent_req): Form<ConsentRequestData>,
380) -> Response<Body> {
381 oauth2_authorise_reject(state, consent_req.token, kopid, client_auth_info).await
382}
383
384pub async fn oauth2_authorise_reject_get(
385 State(state): State<ServerState>,
386 Extension(kopid): Extension<KOpId>,
387 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
388 Query(consent_req): Query<ConsentRequestData>,
389) -> Response<Body> {
390 oauth2_authorise_reject(state, consent_req.token, kopid, client_auth_info).await
391}
392
393async fn oauth2_authorise_reject(
397 state: ServerState,
398 consent_req: String,
399 kopid: KOpId,
400 client_auth_info: ClientAuthInfo,
401) -> Response<Body> {
402 let res = state
406 .qe_r_ref
407 .handle_oauth2_authorise_reject(client_auth_info, consent_req, kopid.eventid)
408 .await;
409
410 match res {
411 Ok(reject) => {
412 let redirect_uri = reject.build_redirect_uri();
413
414 #[allow(clippy::unwrap_used)]
415 Response::builder()
416 .header(LOCATION, redirect_uri.as_str())
417 .header(
418 ACCESS_CONTROL_ALLOW_ORIGIN,
419 redirect_uri.origin().ascii_serialization(),
420 )
421 .body(Body::empty())
422 .unwrap()
423 }
425 Err(err) => {
426 match err {
427 OperationError::NotAuthenticated => {
428 WebError::from(err).response_with_access_control_origin_header()
429 }
430 _ => {
431 #[allow(clippy::expect_used)]
436 Response::builder()
437 .status(StatusCode::INTERNAL_SERVER_ERROR)
438 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
439 .body(Body::empty())
440 .expect("Failed to generate an error response")
441 }
442 }
443 }
444 }
445}
446
447#[axum_macros::debug_handler]
448#[instrument(skip(state, kopid, client_auth_info), level = "DEBUG")]
449pub async fn oauth2_token_post(
450 State(state): State<ServerState>,
451 Extension(kopid): Extension<KOpId>,
452 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
453 Form(tok_req): Form<AccessTokenRequest>,
454) -> impl IntoResponse {
455 match state
462 .qe_w_ref
463 .handle_oauth2_token_exchange(client_auth_info, tok_req, kopid.eventid)
464 .await
465 {
466 Ok(tok_res) => (
467 StatusCode::OK,
468 [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
469 Json(tok_res),
470 )
471 .into_response(),
472 Err(e) => WebError::OAuth2(e).into_response(),
473 }
474}
475
476pub async fn oauth2_openid_discovery_get(
478 State(state): State<ServerState>,
479 Path(client_id): Path<String>,
480 Extension(kopid): Extension<KOpId>,
481) -> impl IntoResponse {
482 let res = state
483 .qe_r_ref
484 .handle_oauth2_openid_discovery(client_id, kopid.eventid)
485 .await;
486
487 match res {
488 Ok(dsc) => (
489 StatusCode::OK,
490 [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
491 Json(dsc),
492 )
493 .into_response(),
494 Err(e) => {
495 error!(err = ?e, "Unable to access discovery info");
496 WebError::from(e).response_with_access_control_origin_header()
497 }
498 }
499}
500
501#[derive(Deserialize)]
502pub struct Oauth2OpenIdWebfingerQuery {
503 resource: String,
504}
505
506pub async fn oauth2_openid_webfinger_get(
507 State(state): State<ServerState>,
508 Path(client_id): Path<String>,
509 Query(query): Query<Oauth2OpenIdWebfingerQuery>,
510 Extension(kopid): Extension<KOpId>,
511) -> impl IntoResponse {
512 let Oauth2OpenIdWebfingerQuery { resource } = query;
513
514 let cleaned_resource = resource.strip_prefix("acct:").unwrap_or(&resource);
515
516 let res = state
517 .qe_r_ref
518 .handle_oauth2_webfinger_discovery(&client_id, cleaned_resource, kopid.eventid)
519 .await;
520
521 match res {
522 Ok(mut dsc) => (
523 StatusCode::OK,
524 [
525 (ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
526 (CONTENT_TYPE, "application/jrd+json"),
527 ],
528 Json({
529 dsc.subject = resource;
530 dsc
531 }),
532 )
533 .into_response(),
534 Err(e) => {
535 error!(err = ?e, "Unable to access discovery info");
536 WebError::from(e).response_with_access_control_origin_header()
537 }
538 }
539}
540
541pub async fn oauth2_rfc8414_metadata_get(
542 State(state): State<ServerState>,
543 Path(client_id): Path<String>,
544 Extension(kopid): Extension<KOpId>,
545) -> impl IntoResponse {
546 let res = state
547 .qe_r_ref
548 .handle_oauth2_rfc8414_metadata(client_id, kopid.eventid)
549 .await;
550
551 match res {
552 Ok(dsc) => (
553 StatusCode::OK,
554 [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
555 Json(dsc),
556 )
557 .into_response(),
558 Err(e) => {
559 error!(err = ?e, "Unable to access discovery info");
560 WebError::from(e).response_with_access_control_origin_header()
561 }
562 }
563}
564
565#[debug_handler]
566pub async fn oauth2_openid_userinfo_get(
567 State(state): State<ServerState>,
568 Path(client_id): Path<String>,
569 Extension(kopid): Extension<KOpId>,
570 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
571) -> Response {
572 let Some(client_token) = client_auth_info.bearer_token() else {
574 error!("Bearer Authentication Not Provided");
575 return WebError::OAuth2(Oauth2Error::AuthenticationRequired).into_response();
576 };
577
578 let res = state
579 .qe_r_ref
580 .handle_oauth2_openid_userinfo(client_id, client_token, kopid.eventid)
581 .await;
582
583 match res {
584 Ok(uir) => (
585 StatusCode::OK,
586 [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
587 Json(uir),
588 )
589 .into_response(),
590 Err(e) => WebError::OAuth2(e).into_response(),
591 }
592}
593
594pub async fn oauth2_openid_publickey_get(
595 State(state): State<ServerState>,
596 Path(client_id): Path<String>,
597 Extension(kopid): Extension<KOpId>,
598) -> Response {
599 let res = state
600 .qe_r_ref
601 .handle_oauth2_openid_publickey(client_id, kopid.eventid)
602 .await
603 .map(Json::from)
604 .map_err(WebError::from);
605
606 match res {
607 Ok(jsn) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], jsn).into_response(),
608 Err(web_err) => web_err.response_with_access_control_origin_header(),
609 }
610}
611
612pub async fn oauth2_token_introspect_post(
615 State(state): State<ServerState>,
616 Extension(kopid): Extension<KOpId>,
617 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
618 Form(intr_req): Form<AccessTokenIntrospectRequest>,
619) -> impl IntoResponse {
620 request_trace!("Introspect Request - {:?}", intr_req);
621 let res = state
622 .qe_r_ref
623 .handle_oauth2_token_introspect(client_auth_info, intr_req, kopid.eventid)
624 .await;
625
626 match res {
627 Ok(atr) => {
628 let body = match serde_json::to_string(&atr) {
629 Ok(val) => val,
630 Err(e) => {
631 admin_warn!("Failed to serialize introspect response: original_data=\"{:?}\" serialization_error=\"{:?}\"", atr, e);
632 format!("{atr:?}")
633 }
634 };
635 #[allow(clippy::unwrap_used)]
636 Response::builder()
637 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
638 .header(CONTENT_TYPE, APPLICATION_JSON)
639 .body(Body::from(body))
640 .unwrap()
641 }
642 Err(Oauth2Error::AuthenticationRequired) => {
643 #[allow(clippy::expect_used)]
645 Response::builder()
646 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
647 .status(StatusCode::UNAUTHORIZED)
648 .body(Body::empty())
649 .expect("Failed to generate an unauthorized response")
650 }
651 Err(e) => {
652 let err = ErrorResponse {
654 error: e.to_string(),
655 ..Default::default()
656 };
657
658 let body = match serde_json::to_string(&err) {
659 Ok(val) => val,
660 Err(e) => {
661 format!("{e:?}")
662 }
663 };
664 #[allow(clippy::expect_used)]
665 Response::builder()
666 .status(StatusCode::BAD_REQUEST)
667 .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
668 .body(Body::from(body))
669 .expect("Failed to generate an error response")
670 }
671 }
672}
673
674pub async fn oauth2_token_revoke_post(
677 State(state): State<ServerState>,
678 Extension(kopid): Extension<KOpId>,
679 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
680 Form(intr_req): Form<TokenRevokeRequest>,
681) -> impl IntoResponse {
682 request_trace!("Revoke Request - {:?}", intr_req);
683
684 let res = state
685 .qe_w_ref
686 .handle_oauth2_token_revoke(client_auth_info, intr_req, kopid.eventid)
687 .await;
688
689 match res {
690 Ok(()) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], "").into_response(),
691 Err(Oauth2Error::AuthenticationRequired) => {
692 (
694 StatusCode::UNAUTHORIZED,
695 [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
696 "",
697 )
698 .into_response()
699 }
700 Err(e) => {
701 let err = ErrorResponse {
703 error: e.to_string(),
704 ..Default::default()
705 };
706 (
707 StatusCode::BAD_REQUEST,
708 [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
709 serde_json::to_string(&err).unwrap_or("".to_string()),
710 )
711 .into_response()
712 }
713 }
714}
715
716pub async fn oauth2_preflight_options() -> Response {
718 (
719 StatusCode::OK,
720 [
721 (ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
722 (ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
723 ],
724 String::new(),
725 )
726 .into_response()
727}
728
729#[serde_as]
730#[derive(Deserialize, Debug, Serialize)]
731pub(crate) struct DeviceFlowForm {
732 client_id: String,
733 #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, String>>")]
734 scope: Option<BTreeSet<String>>,
735 #[serde(flatten)]
736 extra: BTreeMap<String, String>, }
738
739#[cfg(feature = "dev-oauth2-device-flow")]
741#[instrument(level = "info", skip(state, kopid, client_auth_info))]
742pub(crate) async fn oauth2_authorise_device_post(
743 State(state): State<ServerState>,
744 Extension(kopid): Extension<KOpId>,
745 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
746 Form(form): Form<DeviceFlowForm>,
747) -> Result<Json<DeviceAuthorizationResponse>, WebError> {
748 state
749 .qe_w_ref
750 .handle_oauth2_device_flow_start(
751 client_auth_info,
752 &form.client_id,
753 &form.scope,
754 kopid.eventid,
755 )
756 .await
757 .map(Json::from)
758 .map_err(WebError::OAuth2)
759}
760
761pub fn route_setup(state: ServerState) -> Router<ServerState> {
762 let openid_router = Router::new()
764 .route(
767 "/oauth2/openid/:client_id/.well-known/openid-configuration",
768 get(oauth2_openid_discovery_get).options(oauth2_preflight_options),
769 )
770 .route(
771 "/oauth2/openid/:client_id/.well-known/webfinger",
772 get(oauth2_openid_webfinger_get).options(oauth2_preflight_options),
773 )
774 .route(
777 "/oauth2/openid/:client_id/userinfo",
778 get(oauth2_openid_userinfo_get)
779 .post(oauth2_openid_userinfo_get)
780 .options(oauth2_preflight_options),
781 )
782 .route(
785 "/oauth2/openid/:client_id/public_key.jwk",
786 get(oauth2_openid_publickey_get).options(oauth2_preflight_options),
787 )
788 .route(
791 "/oauth2/openid/:client_id/.well-known/oauth-authorization-server",
792 get(oauth2_rfc8414_metadata_get).options(oauth2_preflight_options),
793 )
794 .with_state(state.clone());
795
796 let mut router = Router::new()
797 .route("/oauth2", get(super::v1_oauth2::oauth2_get))
798 .route(
801 OAUTH2_AUTHORISE,
802 post(oauth2_authorise_post).get(oauth2_authorise_get),
803 )
804 .route(
807 OAUTH2_AUTHORISE_PERMIT,
808 post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get),
809 )
810 .route(
813 OAUTH2_AUTHORISE_REJECT,
814 post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get),
815 );
816 #[cfg(feature = "dev-oauth2-device-flow")]
819 {
820 router = router.route(OAUTH2_AUTHORISE_DEVICE, post(oauth2_authorise_device_post))
821 }
822 router = router
825 .route(
826 OAUTH2_TOKEN_ENDPOINT,
827 post(oauth2_token_post).options(oauth2_preflight_options),
828 )
829 .route(
832 OAUTH2_TOKEN_INTROSPECT_ENDPOINT,
833 post(oauth2_token_introspect_post),
834 )
835 .route(OAUTH2_TOKEN_REVOKE_ENDPOINT, post(oauth2_token_revoke_post))
836 .merge(openid_router)
837 .with_state(state)
838 .layer(from_fn(super::middleware::caching::dont_cache_me));
839
840 router
841}