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