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