kanidmd_core/https/
oauth2.rs

1//! OAuth2/OIDC HTTPs handlers
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};
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
48// == Oauth2 Configuration Endpoints ==
49
50/// Get a filter matching a given OAuth2 Resource Server
51pub(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)]
70/// This returns the image for the OAuth2 Resource Server if the user has permissions
71///
72pub(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// == OAUTH2 PROTOCOL FLOW HANDLERS ==
99//
100// oauth2 (partial)
101// https://tools.ietf.org/html/rfc6749
102// oauth2 pkce
103// https://tools.ietf.org/html/rfc7636
104//
105// TODO
106// oauth2 token introspection
107// https://tools.ietf.org/html/rfc7662
108// oauth2 bearer token
109// https://tools.ietf.org/html/rfc6750
110//
111// From https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
112//
113//       +----------+
114//       | Resource |
115//       |   Owner  |
116//       |          |
117//       +----------+
118//            ^
119//            |
120//           (B)
121//       +----|-----+          Client Identifier      +---------------+
122//       |         -+----(A)-- & Redirection URI ---->|               |
123//       |  User-   |                                 | Authorization |
124//       |  Agent  -+----(B)-- User authenticates --->|     Server    |
125//       |          |                                 |               |
126//       |         -+----(C)-- Authorization Code ---<|               |
127//       +-|----|---+                                 +---------------+
128//         |    |                                         ^      v
129//        (A)  (C)                                        |      |
130//         |    |                                         |      |
131//         ^    v                                         |      |
132//       +---------+                                      |      |
133//       |         |>---(D)-- Authorization Code ---------'      |
134//       |  Client |          & Redirection URI                  |
135//       |         |                                             |
136//       |         |<---(E)----- Access Token -------------------'
137//       +---------+       (w/ Optional Refresh Token)
138//
139//     Note: The lines illustrating steps (A), (B), and (C) are broken into
140//     two parts as they pass through the user-agent.
141//
142//  In this diagram, kanidm is the authorisation server. Each step is handled by:
143//
144//  * Client Identifier  A)  oauth2_authorise_get
145//  * User authenticates B)  normal kanidm auth flow
146//  * Authorization Code C)  oauth2_authorise_permit_get
147//                           oauth2_authorise_reject_get
148//  * Authorization Code / Access Token
149//                     D/E)  oauth2_token_post
150//
151//  These functions appear stateless, but the state is managed through encrypted
152//  tokens transmitted in the responses of this flow. This is because in a HA setup
153//  we can not guarantee that the User-Agent or the Resource Server (client) will
154//  access the same Kanidm instance, and we can not rely on replication in these
155//  cases. As a result, we must have our state in localised tokens so that any
156//  valid Kanidm instance in the topology can handle these request.
157//
158
159#[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        // in post, we need the redirect not to be issued, so we mask 302 to 200
171        *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    // Start the oauth2 authorisation flow to present to the user.
184    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            // Render a redirect to the consent page for the user to interact with
206            // to authorise this session-id
207            // This is json so later we can expand it with better detail.
208            #[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            // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
224            // We could consider changing this to 303?
225            #[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                // I think the client server needs this
238                .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            // This will trigger our ui to auth and retry.
248            #[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            // If scopes are not available for this account.
258            #[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        /*
266        RFC - If the request fails due to a missing, invalid, or mismatching
267              redirection URI, or if the client identifier is missing or invalid,
268              the authorization server SHOULD inform the resource owner of the
269              error and MUST NOT automatically redirect the user-agent to the
270              invalid redirection URI.
271        */
272        // To further this, it appears that a malicious client configuration can set a phishing
273        // site as the redirect URL, and then use that to trigger certain types of attacks. Instead
274        // we do NOT redirect in an error condition, and just render the error ourselves.
275        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        // in post, we need the redirect not to be issued, so we mask 302 to 200
302        *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    // When this is called, this indicates consent to proceed from the user.
319    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            // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
336            // We could consider changing this to 303?
337            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                    // If an error happens in our consent flow, I think
357                    // that we should NOT redirect to the calling application
358                    // and we need to handle that locally somehow.
359                    // This needs to be better!
360                    //
361                    // Turns out this instinct was correct:
362                    //  https://www.proofpoint.com/us/blog/cloud-security/microsoft-and-github-oauth-implementation-vulnerabilities-lead-redirection
363                    // Possible to use this with a malicious client configuration to phish / spam.
364                    #[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
376// When this is called, this indicates the user has REJECTED the intent to proceed.
377pub 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
395// // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
396// // If the user willingly rejects the authorisation, we must redirect
397// // with an error.
398async fn oauth2_authorise_reject(
399    state: ServerState,
400    consent_req: String,
401    kopid: KOpId,
402    client_auth_info: ClientAuthInfo,
403) -> Response<Body> {
404    // Need to go back to the redir_uri
405    // For this, we'll need to lookup where to go.
406
407    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            // I think the client server needs this
426        }
427        Err(err) => {
428            match err {
429                OperationError::NotAuthenticated => {
430                    WebError::from(err).response_with_access_control_origin_header()
431                }
432                _ => {
433                    // If an error happens in our reject flow, I think
434                    // that we should NOT redirect to the calling application
435                    // and we need to handle that locally somehow.
436                    // This needs to be better!
437                    #[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    // This is called directly by the resource server, where we then issue
458    // the token to the caller.
459
460    // Do we change the method/path we take here based on the type of requested
461    // grant? Should we cease the delayed/async session update here and just opt
462    // for a wr txn?
463    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
478// For future openid integration
479pub 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    // The token we want to inspect is in the authorisation header.
575    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
614/// This is called directly by the resource server, where we then issue
615/// information about this token to the caller.
616pub 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            // This will trigger our ui to auth and retry.
646            #[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            // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
655            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
676/// This is called directly by the resource server, where we then revoke
677/// the token identified by this request.
678pub 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            // This will trigger our ui to auth and retry.
695            (
696                StatusCode::UNAUTHORIZED,
697                [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
698                "",
699            )
700                .into_response()
701        }
702        Err(e) => {
703            // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
704            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
718// Some requests from browsers require preflight so that CORS works.
719pub 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>, // catches any extra nonsense that gets sent through
740}
741
742/// Device flow! [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628)
743#[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    // this has all the openid-related routes
766    let openid_router = Router::new()
767        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
768        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
769        .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        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
778        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
779        .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        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
786        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
787        .route(
788            "/oauth2/openid/:client_id/public_key.jwk",
789            get(oauth2_openid_publickey_get).options(oauth2_preflight_options),
790        )
791        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
792        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OAUTH2 DISCOVERY URLS
793        .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        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
802        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
803        .route(
804            OAUTH2_AUTHORISE,
805            post(oauth2_authorise_post).get(oauth2_authorise_get),
806        )
807        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
808        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
809        .route(
810            OAUTH2_AUTHORISE_PERMIT,
811            post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get),
812        )
813        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
814        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
815        .route(
816            OAUTH2_AUTHORISE_REJECT,
817            post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get),
818        );
819    // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
820    // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
821    #[cfg(feature = "dev-oauth2-device-flow")]
822    {
823        router = router.route(OAUTH2_AUTHORISE_DEVICE, post(oauth2_authorise_device_post))
824    }
825    // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
826    // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
827    router = router
828        .route(
829            OAUTH2_TOKEN_ENDPOINT,
830            post(oauth2_token_post).options(oauth2_preflight_options),
831        )
832        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
833        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
834        .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}