kanidmd_core/https/
oauth2.rs

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
46// TODO: merge this into a value in WebError later
47pub struct HTTPOauth2Error(Oauth2Error);
48
49impl IntoResponse for HTTPOauth2Error {
50    fn into_response(self) -> Response {
51        let HTTPOauth2Error(error) = self;
52
53        if let Oauth2Error::AuthenticationRequired = error {
54            (
55                StatusCode::UNAUTHORIZED,
56                [
57                    (WWW_AUTHENTICATE, "Bearer"),
58                    (ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
59                ],
60            )
61                .into_response()
62        } else {
63            let err = ErrorResponse {
64                error: error.to_string(),
65                ..Default::default()
66            };
67
68            let body = match serde_json::to_string(&err) {
69                Ok(val) => val,
70                Err(e) => {
71                    admin_warn!("Failed to serialize error response: original_error=\"{:?}\" serialization_error=\"{:?}\"", err, e);
72                    format!("{:?}", err)
73                }
74            };
75
76            (
77                StatusCode::BAD_REQUEST,
78                [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
79                body,
80            )
81                .into_response()
82        }
83    }
84}
85
86// == Oauth2 Configuration Endpoints ==
87
88/// Get a filter matching a given OAuth2 Resource Server
89pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
90    filter_all!(f_and!([
91        f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
92        f_eq(Attribute::Name, PartialValue::new_iname(rs_name))
93    ]))
94}
95
96#[utoipa::path(
97    get,
98    path = "/ui/images/oauth2/{rs_name}",
99    operation_id = "oauth2_image_get",
100    responses(
101        (status = 200, description = "Ok", body=&[u8]),
102        (status = 401, description = "Authorization required"),
103        (status = 403, description = "Not Authorized"),
104    ),
105    security(("token_jwt" = [])),
106    tag = "ui",
107)]
108/// This returns the image for the OAuth2 Resource Server if the user has permissions
109///
110pub(crate) async fn oauth2_image_get(
111    State(state): State<ServerState>,
112    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
113    Path(rs_name): Path<String>,
114) -> Response {
115    let rs_filter = oauth2_id(&rs_name);
116    let res = state
117        .qe_r_ref
118        .handle_oauth2_rs_image_get_image(client_auth_info, rs_filter)
119        .await;
120
121    match res {
122        Ok(Some(image)) => (
123            StatusCode::OK,
124            [(CONTENT_TYPE, image.filetype.as_content_type_str())],
125            image.contents,
126        )
127            .into_response(),
128        Ok(None) => {
129            warn!(?rs_name, "No image set for OAuth2 client");
130            (StatusCode::NOT_FOUND, "").into_response()
131        }
132        Err(err) => WebError::from(err).into_response(),
133    }
134}
135
136// == OAUTH2 PROTOCOL FLOW HANDLERS ==
137//
138// oauth2 (partial)
139// https://tools.ietf.org/html/rfc6749
140// oauth2 pkce
141// https://tools.ietf.org/html/rfc7636
142//
143// TODO
144// oauth2 token introspection
145// https://tools.ietf.org/html/rfc7662
146// oauth2 bearer token
147// https://tools.ietf.org/html/rfc6750
148//
149// From https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
150//
151//       +----------+
152//       | Resource |
153//       |   Owner  |
154//       |          |
155//       +----------+
156//            ^
157//            |
158//           (B)
159//       +----|-----+          Client Identifier      +---------------+
160//       |         -+----(A)-- & Redirection URI ---->|               |
161//       |  User-   |                                 | Authorization |
162//       |  Agent  -+----(B)-- User authenticates --->|     Server    |
163//       |          |                                 |               |
164//       |         -+----(C)-- Authorization Code ---<|               |
165//       +-|----|---+                                 +---------------+
166//         |    |                                         ^      v
167//        (A)  (C)                                        |      |
168//         |    |                                         |      |
169//         ^    v                                         |      |
170//       +---------+                                      |      |
171//       |         |>---(D)-- Authorization Code ---------'      |
172//       |  Client |          & Redirection URI                  |
173//       |         |                                             |
174//       |         |<---(E)----- Access Token -------------------'
175//       +---------+       (w/ Optional Refresh Token)
176//
177//     Note: The lines illustrating steps (A), (B), and (C) are broken into
178//     two parts as they pass through the user-agent.
179//
180//  In this diagram, kanidm is the authorisation server. Each step is handled by:
181//
182//  * Client Identifier  A)  oauth2_authorise_get
183//  * User authenticates B)  normal kanidm auth flow
184//  * Authorization Code C)  oauth2_authorise_permit_get
185//                           oauth2_authorise_reject_get
186//  * Authorization Code / Access Token
187//                     D/E)  oauth2_token_post
188//
189//  These functions appear stateless, but the state is managed through encrypted
190//  tokens transmitted in the responses of this flow. This is because in a HA setup
191//  we can not guarantee that the User-Agent or the Resource Server (client) will
192//  access the same Kanidm instance, and we can not rely on replication in these
193//  cases. As a result, we must have our state in localised tokens so that any
194//  valid Kanidm instance in the topology can handle these request.
195//
196
197#[instrument(level = "debug", skip(state, kopid))]
198pub async fn oauth2_authorise_post(
199    State(state): State<ServerState>,
200    Extension(kopid): Extension<KOpId>,
201    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
202    Json(auth_req): Json<AuthorisationRequest>,
203) -> impl IntoResponse {
204    let mut res = oauth2_authorise(state, auth_req, kopid, client_auth_info)
205        .await
206        .into_response();
207    if res.status() == StatusCode::FOUND {
208        // in post, we need the redirect not to be issued, so we mask 302 to 200
209        *res.status_mut() = StatusCode::OK;
210    }
211    res
212}
213
214#[instrument(level = "debug", skip(state, kopid))]
215pub async fn oauth2_authorise_get(
216    State(state): State<ServerState>,
217    Extension(kopid): Extension<KOpId>,
218    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
219    Query(auth_req): Query<AuthorisationRequest>,
220) -> impl IntoResponse {
221    // Start the oauth2 authorisation flow to present to the user.
222    oauth2_authorise(state, auth_req, kopid, client_auth_info).await
223}
224
225async fn oauth2_authorise(
226    state: ServerState,
227    auth_req: AuthorisationRequest,
228    kopid: KOpId,
229    client_auth_info: ClientAuthInfo,
230) -> impl IntoResponse {
231    let res: Result<AuthoriseResponse, Oauth2Error> = state
232        .qe_r_ref
233        .handle_oauth2_authorise(client_auth_info, auth_req, kopid.eventid)
234        .await;
235
236    match res {
237        Ok(AuthoriseResponse::ConsentRequested {
238            client_name,
239            scopes,
240            pii_scopes,
241            consent_token,
242        }) => {
243            // Render a redirect to the consent page for the user to interact with
244            // to authorise this session-id
245            // This is json so later we can expand it with better detail.
246            #[allow(clippy::unwrap_used)]
247            let body = serde_json::to_string(&AuthorisationResponse::ConsentRequested {
248                client_name,
249                scopes,
250                pii_scopes,
251                consent_token,
252            })
253            .unwrap();
254            #[allow(clippy::unwrap_used)]
255            Response::builder()
256                .status(StatusCode::OK)
257                .body(body.into())
258                .unwrap()
259        }
260        Ok(AuthoriseResponse::Permitted(success)) => {
261            // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
262            // We could consider changing this to 303?
263            #[allow(clippy::unwrap_used)]
264            let body =
265                Body::from(serde_json::to_string(&AuthorisationResponse::Permitted).unwrap());
266            let redirect_uri = success.build_redirect_uri();
267
268            #[allow(clippy::unwrap_used)]
269            Response::builder()
270                .status(StatusCode::FOUND)
271                .header(
272                    LOCATION,
273                    HeaderValue::from_str(redirect_uri.as_str()).unwrap(),
274                )
275                // I think the client server needs this
276                .header(
277                    ACCESS_CONTROL_ALLOW_ORIGIN,
278                    HeaderValue::from_str(&redirect_uri.origin().ascii_serialization()).unwrap(),
279                )
280                .body(body)
281                .unwrap()
282        }
283        Ok(AuthoriseResponse::AuthenticationRequired { .. })
284        | Err(Oauth2Error::AuthenticationRequired) => {
285            // This will trigger our ui to auth and retry.
286            #[allow(clippy::unwrap_used)]
287            Response::builder()
288                .status(StatusCode::UNAUTHORIZED)
289                .header(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"))
290                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
291                .body(Body::empty())
292                .unwrap()
293        }
294        Err(Oauth2Error::AccessDenied) => {
295            // If scopes are not available for this account.
296            #[allow(clippy::expect_used)]
297            Response::builder()
298                .status(StatusCode::FORBIDDEN)
299                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
300                .body(Body::empty())
301                .expect("Failed to generate a forbidden response")
302        }
303        /*
304        RFC - If the request fails due to a missing, invalid, or mismatching
305              redirection URI, or if the client identifier is missing or invalid,
306              the authorization server SHOULD inform the resource owner of the
307              error and MUST NOT automatically redirect the user-agent to the
308              invalid redirection URI.
309        */
310        // To further this, it appears that a malicious client configuration can set a phishing
311        // site as the redirect URL, and then use that to trigger certain types of attacks. Instead
312        // we do NOT redirect in an error condition, and just render the error ourselves.
313        Err(e) => {
314            admin_error!(
315                "Unable to authorise - Error ID: {:?} error: {}",
316                kopid.eventid,
317                &e.to_string()
318            );
319            #[allow(clippy::expect_used)]
320            Response::builder()
321                .status(StatusCode::BAD_REQUEST)
322                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
323                .body(Body::empty())
324                .expect("Failed to generate a bad request response")
325        }
326    }
327}
328
329pub async fn oauth2_authorise_permit_post(
330    State(state): State<ServerState>,
331    Extension(kopid): Extension<KOpId>,
332    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
333    Json(consent_req): Json<String>,
334) -> impl IntoResponse {
335    let mut res = oauth2_authorise_permit(state, consent_req, kopid, client_auth_info)
336        .await
337        .into_response();
338    if res.status() == StatusCode::FOUND {
339        // in post, we need the redirect not to be issued, so we mask 302 to 200
340        *res.status_mut() = StatusCode::OK;
341    }
342    res
343}
344
345#[derive(Serialize, Deserialize, Debug)]
346pub struct ConsentRequestData {
347    token: String,
348}
349
350pub async fn oauth2_authorise_permit_get(
351    State(state): State<ServerState>,
352    Query(token): Query<ConsentRequestData>,
353    Extension(kopid): Extension<KOpId>,
354    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
355) -> impl IntoResponse {
356    // When this is called, this indicates consent to proceed from the user.
357    oauth2_authorise_permit(state, token.token, kopid, client_auth_info).await
358}
359
360async fn oauth2_authorise_permit(
361    state: ServerState,
362    consent_req: String,
363    kopid: KOpId,
364    client_auth_info: ClientAuthInfo,
365) -> impl IntoResponse {
366    let res = state
367        .qe_w_ref
368        .handle_oauth2_authorise_permit(client_auth_info, consent_req, kopid.eventid)
369        .await;
370
371    match res {
372        Ok(success) => {
373            // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
374            // We could consider changing this to 303?
375            let redirect_uri = success.build_redirect_uri();
376
377            #[allow(clippy::expect_used)]
378            Response::builder()
379                .status(StatusCode::FOUND)
380                .header(LOCATION, redirect_uri.as_str())
381                .header(
382                    ACCESS_CONTROL_ALLOW_ORIGIN,
383                    redirect_uri.origin().ascii_serialization(),
384                )
385                .body(Body::empty())
386                .expect("Failed to generate response")
387        }
388        Err(err) => {
389            match err {
390                OperationError::NotAuthenticated => {
391                    WebError::from(err).response_with_access_control_origin_header()
392                }
393                _ => {
394                    // If an error happens in our consent flow, I think
395                    // that we should NOT redirect to the calling application
396                    // and we need to handle that locally somehow.
397                    // This needs to be better!
398                    //
399                    // Turns out this instinct was correct:
400                    //  https://www.proofpoint.com/us/blog/cloud-security/microsoft-and-github-oauth-implementation-vulnerabilities-lead-redirection
401                    // Possible to use this with a malicious client configuration to phish / spam.
402                    #[allow(clippy::expect_used)]
403                    Response::builder()
404                        .status(StatusCode::INTERNAL_SERVER_ERROR)
405                        .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
406                        .body(Body::empty())
407                        .expect("Failed to generate error response")
408                }
409            }
410        }
411    }
412}
413
414// When this is called, this indicates the user has REJECTED the intent to proceed.
415pub async fn oauth2_authorise_reject_post(
416    State(state): State<ServerState>,
417    Extension(kopid): Extension<KOpId>,
418    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
419    Form(consent_req): Form<ConsentRequestData>,
420) -> Response<Body> {
421    oauth2_authorise_reject(state, consent_req.token, kopid, client_auth_info).await
422}
423
424pub async fn oauth2_authorise_reject_get(
425    State(state): State<ServerState>,
426    Extension(kopid): Extension<KOpId>,
427    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
428    Query(consent_req): Query<ConsentRequestData>,
429) -> Response<Body> {
430    oauth2_authorise_reject(state, consent_req.token, kopid, client_auth_info).await
431}
432
433// // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
434// // If the user willingly rejects the authorisation, we must redirect
435// // with an error.
436async fn oauth2_authorise_reject(
437    state: ServerState,
438    consent_req: String,
439    kopid: KOpId,
440    client_auth_info: ClientAuthInfo,
441) -> Response<Body> {
442    // Need to go back to the redir_uri
443    // For this, we'll need to lookup where to go.
444
445    let res = state
446        .qe_r_ref
447        .handle_oauth2_authorise_reject(client_auth_info, consent_req, kopid.eventid)
448        .await;
449
450    match res {
451        Ok(reject) => {
452            let redirect_uri = reject.build_redirect_uri();
453
454            #[allow(clippy::unwrap_used)]
455            Response::builder()
456                .header(LOCATION, redirect_uri.as_str())
457                .header(
458                    ACCESS_CONTROL_ALLOW_ORIGIN,
459                    redirect_uri.origin().ascii_serialization(),
460                )
461                .body(Body::empty())
462                .unwrap()
463            // I think the client server needs this
464        }
465        Err(err) => {
466            match err {
467                OperationError::NotAuthenticated => {
468                    WebError::from(err).response_with_access_control_origin_header()
469                }
470                _ => {
471                    // If an error happens in our reject flow, I think
472                    // that we should NOT redirect to the calling application
473                    // and we need to handle that locally somehow.
474                    // This needs to be better!
475                    #[allow(clippy::expect_used)]
476                    Response::builder()
477                        .status(StatusCode::INTERNAL_SERVER_ERROR)
478                        .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
479                        .body(Body::empty())
480                        .expect("Failed to generate an error response")
481                }
482            }
483        }
484    }
485}
486
487#[axum_macros::debug_handler]
488#[instrument(skip(state, kopid, client_auth_info), level = "DEBUG")]
489pub async fn oauth2_token_post(
490    State(state): State<ServerState>,
491    Extension(kopid): Extension<KOpId>,
492    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
493    Form(tok_req): Form<AccessTokenRequest>,
494) -> impl IntoResponse {
495    // This is called directly by the resource server, where we then issue
496    // the token to the caller.
497
498    // Do we change the method/path we take here based on the type of requested
499    // grant? Should we cease the delayed/async session update here and just opt
500    // for a wr txn?
501    match state
502        .qe_w_ref
503        .handle_oauth2_token_exchange(client_auth_info, tok_req, kopid.eventid)
504        .await
505    {
506        Ok(tok_res) => (
507            StatusCode::OK,
508            [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
509            Json(tok_res),
510        )
511            .into_response(),
512        Err(e) => HTTPOauth2Error(e).into_response(),
513    }
514}
515
516// For future openid integration
517pub async fn oauth2_openid_discovery_get(
518    State(state): State<ServerState>,
519    Path(client_id): Path<String>,
520    Extension(kopid): Extension<KOpId>,
521) -> impl IntoResponse {
522    let res = state
523        .qe_r_ref
524        .handle_oauth2_openid_discovery(client_id, kopid.eventid)
525        .await;
526
527    match res {
528        Ok(dsc) => (
529            StatusCode::OK,
530            [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
531            Json(dsc),
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
541#[derive(Deserialize)]
542pub struct Oauth2OpenIdWebfingerQuery {
543    resource: String,
544}
545
546pub async fn oauth2_openid_webfinger_get(
547    State(state): State<ServerState>,
548    Path(client_id): Path<String>,
549    Query(query): Query<Oauth2OpenIdWebfingerQuery>,
550    Extension(kopid): Extension<KOpId>,
551) -> impl IntoResponse {
552    let Oauth2OpenIdWebfingerQuery { resource } = query;
553
554    let cleaned_resource = resource.strip_prefix("acct:").unwrap_or(&resource);
555
556    let res = state
557        .qe_r_ref
558        .handle_oauth2_webfinger_discovery(&client_id, cleaned_resource, kopid.eventid)
559        .await;
560
561    match res {
562        Ok(mut dsc) => (
563            StatusCode::OK,
564            [
565                (ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
566                (CONTENT_TYPE, "application/jrd+json"),
567            ],
568            Json({
569                dsc.subject = resource;
570                dsc
571            }),
572        )
573            .into_response(),
574        Err(e) => {
575            error!(err = ?e, "Unable to access discovery info");
576            WebError::from(e).response_with_access_control_origin_header()
577        }
578    }
579}
580
581pub async fn oauth2_rfc8414_metadata_get(
582    State(state): State<ServerState>,
583    Path(client_id): Path<String>,
584    Extension(kopid): Extension<KOpId>,
585) -> impl IntoResponse {
586    let res = state
587        .qe_r_ref
588        .handle_oauth2_rfc8414_metadata(client_id, kopid.eventid)
589        .await;
590
591    match res {
592        Ok(dsc) => (
593            StatusCode::OK,
594            [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
595            Json(dsc),
596        )
597            .into_response(),
598        Err(e) => {
599            error!(err = ?e, "Unable to access discovery info");
600            WebError::from(e).response_with_access_control_origin_header()
601        }
602    }
603}
604
605#[debug_handler]
606pub async fn oauth2_openid_userinfo_get(
607    State(state): State<ServerState>,
608    Path(client_id): Path<String>,
609    Extension(kopid): Extension<KOpId>,
610    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
611) -> Response {
612    // The token we want to inspect is in the authorisation header.
613    let client_token = match client_auth_info.bearer_token {
614        Some(val) => val,
615        None => {
616            error!("Bearer Authentication Not Provided");
617            return HTTPOauth2Error(Oauth2Error::AuthenticationRequired).into_response();
618        }
619    };
620
621    let res = state
622        .qe_r_ref
623        .handle_oauth2_openid_userinfo(client_id, client_token, kopid.eventid)
624        .await;
625
626    match res {
627        Ok(uir) => (
628            StatusCode::OK,
629            [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
630            Json(uir),
631        )
632            .into_response(),
633        Err(e) => HTTPOauth2Error(e).into_response(),
634    }
635}
636
637pub async fn oauth2_openid_publickey_get(
638    State(state): State<ServerState>,
639    Path(client_id): Path<String>,
640    Extension(kopid): Extension<KOpId>,
641) -> Response {
642    let res = state
643        .qe_r_ref
644        .handle_oauth2_openid_publickey(client_id, kopid.eventid)
645        .await
646        .map(Json::from)
647        .map_err(WebError::from);
648
649    match res {
650        Ok(jsn) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], jsn).into_response(),
651        Err(web_err) => web_err.response_with_access_control_origin_header(),
652    }
653}
654
655/// This is called directly by the resource server, where we then issue
656/// information about this token to the caller.
657pub async fn oauth2_token_introspect_post(
658    State(state): State<ServerState>,
659    Extension(kopid): Extension<KOpId>,
660    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
661    Form(intr_req): Form<AccessTokenIntrospectRequest>,
662) -> impl IntoResponse {
663    request_trace!("Introspect Request - {:?}", intr_req);
664    let res = state
665        .qe_r_ref
666        .handle_oauth2_token_introspect(client_auth_info, intr_req, kopid.eventid)
667        .await;
668
669    match res {
670        Ok(atr) => {
671            let body = match serde_json::to_string(&atr) {
672                Ok(val) => val,
673                Err(e) => {
674                    admin_warn!("Failed to serialize introspect response: original_data=\"{:?}\" serialization_error=\"{:?}\"", atr, e);
675                    format!("{:?}", atr)
676                }
677            };
678            #[allow(clippy::unwrap_used)]
679            Response::builder()
680                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
681                .header(CONTENT_TYPE, APPLICATION_JSON)
682                .body(Body::from(body))
683                .unwrap()
684        }
685        Err(Oauth2Error::AuthenticationRequired) => {
686            // This will trigger our ui to auth and retry.
687            #[allow(clippy::expect_used)]
688            Response::builder()
689                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
690                .status(StatusCode::UNAUTHORIZED)
691                .body(Body::empty())
692                .expect("Failed to generate an unauthorized response")
693        }
694        Err(e) => {
695            // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
696            let err = ErrorResponse {
697                error: e.to_string(),
698                ..Default::default()
699            };
700
701            let body = match serde_json::to_string(&err) {
702                Ok(val) => val,
703                Err(e) => {
704                    format!("{:?}", e)
705                }
706            };
707            #[allow(clippy::expect_used)]
708            Response::builder()
709                .status(StatusCode::BAD_REQUEST)
710                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
711                .body(Body::from(body))
712                .expect("Failed to generate an error response")
713        }
714    }
715}
716
717/// This is called directly by the resource server, where we then revoke
718/// the token identified by this request.
719pub async fn oauth2_token_revoke_post(
720    State(state): State<ServerState>,
721    Extension(kopid): Extension<KOpId>,
722    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
723    Form(intr_req): Form<TokenRevokeRequest>,
724) -> impl IntoResponse {
725    request_trace!("Revoke Request - {:?}", intr_req);
726
727    let res = state
728        .qe_w_ref
729        .handle_oauth2_token_revoke(client_auth_info, intr_req, kopid.eventid)
730        .await;
731
732    match res {
733        Ok(()) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], "").into_response(),
734        Err(Oauth2Error::AuthenticationRequired) => {
735            // This will trigger our ui to auth and retry.
736            (
737                StatusCode::UNAUTHORIZED,
738                [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
739                "",
740            )
741                .into_response()
742        }
743        Err(e) => {
744            // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
745            let err = ErrorResponse {
746                error: e.to_string(),
747                ..Default::default()
748            };
749            (
750                StatusCode::BAD_REQUEST,
751                [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
752                serde_json::to_string(&err).unwrap_or("".to_string()),
753            )
754                .into_response()
755        }
756    }
757}
758
759// Some requests from browsers require preflight so that CORS works.
760pub async fn oauth2_preflight_options() -> Response {
761    (
762        StatusCode::OK,
763        [
764            (ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
765            (ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
766        ],
767        String::new(),
768    )
769        .into_response()
770}
771
772#[serde_as]
773#[derive(Deserialize, Debug, Serialize)]
774pub(crate) struct DeviceFlowForm {
775    client_id: String,
776    #[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, String>>")]
777    scope: Option<BTreeSet<String>>,
778    #[serde(flatten)]
779    extra: BTreeMap<String, String>, // catches any extra nonsense that gets sent through
780}
781
782/// Device flow! [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628)
783#[cfg(feature = "dev-oauth2-device-flow")]
784#[instrument(level = "info", skip(state, kopid, client_auth_info))]
785pub(crate) async fn oauth2_authorise_device_post(
786    State(state): State<ServerState>,
787    Extension(kopid): Extension<KOpId>,
788    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
789    Form(form): Form<DeviceFlowForm>,
790) -> Result<Json<DeviceAuthorizationResponse>, HTTPOauth2Error> {
791    state
792        .qe_w_ref
793        .handle_oauth2_device_flow_start(
794            client_auth_info,
795            &form.client_id,
796            &form.scope,
797            kopid.eventid,
798        )
799        .await
800        .map(Json::from)
801        .map_err(HTTPOauth2Error)
802}
803
804pub fn route_setup(state: ServerState) -> Router<ServerState> {
805    // this has all the openid-related routes
806    let openid_router = Router::new()
807        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
808        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
809        .route(
810            "/oauth2/openid/:client_id/.well-known/openid-configuration",
811            get(oauth2_openid_discovery_get).options(oauth2_preflight_options),
812        )
813        .route(
814            "/oauth2/openid/:client_id/.well-known/webfinger",
815            get(oauth2_openid_webfinger_get).options(oauth2_preflight_options),
816        )
817        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
818        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
819        .route(
820            "/oauth2/openid/:client_id/userinfo",
821            get(oauth2_openid_userinfo_get)
822                .post(oauth2_openid_userinfo_get)
823                .options(oauth2_preflight_options),
824        )
825        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
826        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
827        .route(
828            "/oauth2/openid/:client_id/public_key.jwk",
829            get(oauth2_openid_publickey_get).options(oauth2_preflight_options),
830        )
831        // // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
832        // // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OAUTH2 DISCOVERY URLS
833        .route(
834            "/oauth2/openid/:client_id/.well-known/oauth-authorization-server",
835            get(oauth2_rfc8414_metadata_get).options(oauth2_preflight_options),
836        )
837        .with_state(state.clone());
838
839    let mut router = Router::new()
840        .route("/oauth2", get(super::v1_oauth2::oauth2_get))
841        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
842        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
843        .route(
844            OAUTH2_AUTHORISE,
845            post(oauth2_authorise_post).get(oauth2_authorise_get),
846        )
847        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
848        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
849        .route(
850            OAUTH2_AUTHORISE_PERMIT,
851            post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get),
852        )
853        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
854        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
855        .route(
856            OAUTH2_AUTHORISE_REJECT,
857            post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get),
858        );
859    // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
860    // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
861    #[cfg(feature = "dev-oauth2-device-flow")]
862    {
863        router = router.route(OAUTH2_AUTHORISE_DEVICE, post(oauth2_authorise_device_post))
864    }
865    // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
866    // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
867    router = router
868        .route(
869            OAUTH2_TOKEN_ENDPOINT,
870            post(oauth2_token_post).options(oauth2_preflight_options),
871        )
872        // ⚠️  ⚠️   WARNING  ⚠️  ⚠️
873        // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
874        .route(
875            OAUTH2_TOKEN_INTROSPECT_ENDPOINT,
876            post(oauth2_token_introspect_post),
877        )
878        .route(OAUTH2_TOKEN_REVOKE_ENDPOINT, post(oauth2_token_revoke_post))
879        .merge(openid_router)
880        .with_state(state)
881        .layer(from_fn(super::middleware::caching::dont_cache_me));
882
883    router
884}