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 client_token = match client_auth_info.bearer_token {
574        Some(val) => val,
575        None => {
576            error!("Bearer Authentication Not Provided");
577            return WebError::OAuth2(Oauth2Error::AuthenticationRequired).into_response();
578        }
579    };
580
581    let res = state
582        .qe_r_ref
583        .handle_oauth2_openid_userinfo(client_id, client_token, kopid.eventid)
584        .await;
585
586    match res {
587        Ok(uir) => (
588            StatusCode::OK,
589            [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
590            Json(uir),
591        )
592            .into_response(),
593        Err(e) => WebError::OAuth2(e).into_response(),
594    }
595}
596
597pub async fn oauth2_openid_publickey_get(
598    State(state): State<ServerState>,
599    Path(client_id): Path<String>,
600    Extension(kopid): Extension<KOpId>,
601) -> Response {
602    let res = state
603        .qe_r_ref
604        .handle_oauth2_openid_publickey(client_id, kopid.eventid)
605        .await
606        .map(Json::from)
607        .map_err(WebError::from);
608
609    match res {
610        Ok(jsn) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], jsn).into_response(),
611        Err(web_err) => web_err.response_with_access_control_origin_header(),
612    }
613}
614
615/// This is called directly by the resource server, where we then issue
616/// information about this token to the caller.
617pub async fn oauth2_token_introspect_post(
618    State(state): State<ServerState>,
619    Extension(kopid): Extension<KOpId>,
620    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
621    Form(intr_req): Form<AccessTokenIntrospectRequest>,
622) -> impl IntoResponse {
623    request_trace!("Introspect Request - {:?}", intr_req);
624    let res = state
625        .qe_r_ref
626        .handle_oauth2_token_introspect(client_auth_info, intr_req, kopid.eventid)
627        .await;
628
629    match res {
630        Ok(atr) => {
631            let body = match serde_json::to_string(&atr) {
632                Ok(val) => val,
633                Err(e) => {
634                    admin_warn!("Failed to serialize introspect response: original_data=\"{:?}\" serialization_error=\"{:?}\"", atr, e);
635                    format!("{:?}", atr)
636                }
637            };
638            #[allow(clippy::unwrap_used)]
639            Response::builder()
640                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
641                .header(CONTENT_TYPE, APPLICATION_JSON)
642                .body(Body::from(body))
643                .unwrap()
644        }
645        Err(Oauth2Error::AuthenticationRequired) => {
646            // This will trigger our ui to auth and retry.
647            #[allow(clippy::expect_used)]
648            Response::builder()
649                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
650                .status(StatusCode::UNAUTHORIZED)
651                .body(Body::empty())
652                .expect("Failed to generate an unauthorized response")
653        }
654        Err(e) => {
655            // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
656            let err = ErrorResponse {
657                error: e.to_string(),
658                ..Default::default()
659            };
660
661            let body = match serde_json::to_string(&err) {
662                Ok(val) => val,
663                Err(e) => {
664                    format!("{:?}", e)
665                }
666            };
667            #[allow(clippy::expect_used)]
668            Response::builder()
669                .status(StatusCode::BAD_REQUEST)
670                .header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
671                .body(Body::from(body))
672                .expect("Failed to generate an error response")
673        }
674    }
675}
676
677/// This is called directly by the resource server, where we then revoke
678/// the token identified by this request.
679pub async fn oauth2_token_revoke_post(
680    State(state): State<ServerState>,
681    Extension(kopid): Extension<KOpId>,
682    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
683    Form(intr_req): Form<TokenRevokeRequest>,
684) -> impl IntoResponse {
685    request_trace!("Revoke Request - {:?}", intr_req);
686
687    let res = state
688        .qe_w_ref
689        .handle_oauth2_token_revoke(client_auth_info, intr_req, kopid.eventid)
690        .await;
691
692    match res {
693        Ok(()) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], "").into_response(),
694        Err(Oauth2Error::AuthenticationRequired) => {
695            // This will trigger our ui to auth and retry.
696            (
697                StatusCode::UNAUTHORIZED,
698                [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
699                "",
700            )
701                .into_response()
702        }
703        Err(e) => {
704            // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
705            let err = ErrorResponse {
706                error: e.to_string(),
707                ..Default::default()
708            };
709            (
710                StatusCode::BAD_REQUEST,
711                [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
712                serde_json::to_string(&err).unwrap_or("".to_string()),
713            )
714                .into_response()
715        }
716    }
717}
718
719// Some requests from browsers require preflight so that CORS works.
720pub async fn oauth2_preflight_options() -> Response {
721    (
722        StatusCode::OK,
723        [
724            (ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
725            (ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
726        ],
727        String::new(),
728    )
729        .into_response()
730}
731
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<BTreeSet<String>>,
738    #[serde(flatten)]
739    extra: 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}