1use crate::https::{
2    extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation},
3    middleware::KOpId,
4    ServerState,
5};
6use kanidmd_lib::idm::oauth2::{AuthorisationRequest, AuthoriseResponse, Oauth2Error};
7use kanidmd_lib::prelude::*;
8
9use kanidm_proto::internal::COOKIE_OAUTH2_REQ;
10
11use std::collections::BTreeSet;
12
13use askama::Template;
14use askama_web::WebTemplate;
15
16#[cfg(feature = "dev-oauth2-device-flow")]
17use axum::http::StatusCode;
18use axum::{
19    extract::{Query, State},
20    http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
21    response::{IntoResponse, Redirect, Response},
22    Extension, Form,
23};
24use axum_extra::extract::cookie::{CookieJar, SameSite};
25use axum_htmx::HX_REDIRECT;
26use serde::Deserialize;
27
28use super::login::{LoginDisplayCtx, Oauth2Ctx};
29use super::{cookies, UnrecoverableErrorView};
30
31#[derive(Template, WebTemplate)]
32#[template(path = "oauth2_consent_request.html")]
33struct ConsentRequestView {
34    client_name: String,
35    pii_scopes: BTreeSet<String>,
37    consent_token: String,
38    redirect: Option<String>,
39}
40
41#[derive(Template, WebTemplate)]
42#[template(path = "oauth2_access_denied.html")]
43struct AccessDeniedView {
44    operation_id: Uuid,
45}
46
47pub async fn view_index_get(
48    State(state): State<ServerState>,
49    Extension(kopid): Extension<KOpId>,
50    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
51    DomainInfo(domain_info): DomainInfo,
52    jar: CookieJar,
53    Query(auth_req): Query<AuthorisationRequest>,
54) -> Response {
55    oauth2_auth_req(
56        state,
57        kopid,
58        client_auth_info,
59        domain_info,
60        jar,
61        Some(auth_req),
62    )
63    .await
64}
65
66pub async fn view_resume_get(
67    State(state): State<ServerState>,
68    Extension(kopid): Extension<KOpId>,
69    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
70    DomainInfo(domain_info): DomainInfo,
71    jar: CookieJar,
72) -> Response {
73    let maybe_auth_req =
74        cookies::get_signed::<AuthorisationRequest>(&state, &jar, COOKIE_OAUTH2_REQ);
75
76    oauth2_auth_req(
77        state,
78        kopid,
79        client_auth_info,
80        domain_info,
81        jar,
82        maybe_auth_req,
83    )
84    .await
85}
86
87async fn oauth2_auth_req(
88    state: ServerState,
89    kopid: KOpId,
90    client_auth_info: ClientAuthInfo,
91    domain_info: DomainInfoRead,
92    jar: CookieJar,
93    maybe_auth_req: Option<AuthorisationRequest>,
94) -> Response {
95    let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &state);
98
99    let Some(auth_req) = maybe_auth_req else {
102        error!("unable to resume session, no valid auth_req was found in the cookie. This cookie has been removed.");
103        return (
104            jar,
105            UnrecoverableErrorView {
106                err_code: OperationError::UI0003InvalidOauth2Resume,
107                operation_id: kopid.eventid,
108                domain_info,
109            },
110        )
111            .into_response();
112    };
113
114    let res: Result<AuthoriseResponse, Oauth2Error> = state
115        .qe_r_ref
116        .handle_oauth2_authorise(client_auth_info, auth_req.clone(), kopid.eventid)
117        .await;
118
119    match res {
120        Ok(AuthoriseResponse::Permitted(success)) => {
121            let redirect_uri = success.build_redirect_uri();
122
123            (
124                jar,
125                [
126                    (HX_REDIRECT, redirect_uri.as_str().to_string()),
127                    (
128                        ACCESS_CONTROL_ALLOW_ORIGIN,
129                        redirect_uri.origin().ascii_serialization(),
130                    ),
131                ],
132                Redirect::to(redirect_uri.as_str()),
133            )
134                .into_response()
135        }
136        Ok(AuthoriseResponse::ConsentRequested {
137            client_name,
138            scopes: _,
139            pii_scopes,
140            consent_token,
141        }) => {
142            (
144                jar,
145                ConsentRequestView {
146                    client_name,
147                    pii_scopes,
149                    consent_token,
150                    redirect: None,
151                },
152            )
153                .into_response()
154        }
155
156        Ok(AuthoriseResponse::AuthenticationRequired {
157            client_name,
158            login_hint,
159        }) => {
160            let maybe_jar = cookies::make_signed(&state, COOKIE_OAUTH2_REQ, &auth_req)
163                .map(|mut cookie| {
164                    cookie.set_same_site(SameSite::Strict);
165                    cookie.set_expires(None);
167                    cookie.set_max_age(time::Duration::minutes(15));
169                    jar.clone().add(cookie)
170                })
171                .ok_or(OperationError::InvalidSessionState);
172
173            match maybe_jar {
174                Ok(new_jar) => {
175                    let display_ctx = LoginDisplayCtx {
176                        domain_info,
177                        oauth2: Some(Oauth2Ctx { client_name }),
178                        reauth: None,
179                        error: None,
180                    };
181
182                    super::login::view_oauth2_get(new_jar, display_ctx, login_hint)
183                }
184                Err(err_code) => (
185                    jar,
186                    UnrecoverableErrorView {
187                        err_code,
188                        operation_id: kopid.eventid,
189                        domain_info,
190                    },
191                )
192                    .into_response(),
193            }
194        }
195        Err(Oauth2Error::AccessDenied) => {
196            (
198                jar,
199                AccessDeniedView {
200                    operation_id: kopid.eventid,
201                },
202            )
203                .into_response()
204        }
205        Err(err_code) => {
216            error!(
217                "Unable to authorise - Error ID: {:?} error: {}",
218                kopid.eventid,
219                &err_code.to_string()
220            );
221
222            (
223                jar,
224                UnrecoverableErrorView {
225                    err_code: OperationError::InvalidState,
226                    operation_id: kopid.eventid,
227                    domain_info,
228                },
229            )
230                .into_response()
231        }
232    }
233}
234
235#[derive(Debug, Clone, Deserialize)]
236pub struct ConsentForm {
237    consent_token: String,
238    #[serde(default)]
239    #[allow(dead_code)] redirect: Option<String>,
241}
242
243pub async fn view_consent_post(
244    State(server_state): State<ServerState>,
245    Extension(kopid): Extension<KOpId>,
246    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
247    DomainInfo(domain_info): DomainInfo,
248    jar: CookieJar,
249    Form(consent_form): Form<ConsentForm>,
250) -> Result<Response, UnrecoverableErrorView> {
251    let res = server_state
252        .qe_w_ref
253        .handle_oauth2_authorise_permit(client_auth_info, consent_form.consent_token, kopid.eventid)
254        .await;
255
256    match res {
257        Ok(success) => {
258            let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &server_state);
259
260            if let Some(redirect) = consent_form.redirect {
261                Ok((
262                    jar,
263                    [
264                        (HX_REDIRECT, success.redirect_uri.as_str().to_string()),
265                        (
266                            ACCESS_CONTROL_ALLOW_ORIGIN,
267                            success.redirect_uri.origin().ascii_serialization(),
268                        ),
269                    ],
270                    Redirect::to(&redirect),
271                )
272                    .into_response())
273            } else {
274                let redirect_uri = success.build_redirect_uri();
275                Ok((
276                    jar,
277                    [
278                        (HX_REDIRECT, redirect_uri.as_str().to_string()),
279                        (
280                            ACCESS_CONTROL_ALLOW_ORIGIN,
281                            redirect_uri.origin().ascii_serialization(),
282                        ),
283                    ],
284                    Redirect::to(redirect_uri.as_str()),
285                )
286                    .into_response())
287            }
288        }
289        Err(err_code) => {
290            error!(
291                "Unable to authorise - Error ID: {:?} error: {}",
292                kopid.eventid,
293                &err_code.to_string()
294            );
295
296            Err(UnrecoverableErrorView {
297                err_code: OperationError::InvalidState,
298                operation_id: kopid.eventid,
299                domain_info,
300            })
301        }
302    }
303}
304
305#[derive(Template, Debug, Clone, WebTemplate)]
306#[cfg(feature = "dev-oauth2-device-flow")]
307#[template(path = "oauth2_device_login.html")]
308pub struct Oauth2DeviceLoginView {
309    domain_custom_image: bool,
310    title: String,
311    user_code: String,
312}
313
314#[derive(Debug, Clone, Deserialize)]
315#[cfg(feature = "dev-oauth2-device-flow")]
316pub(crate) struct QueryUserCode {
317    pub user_code: Option<String>,
318}
319
320#[axum::debug_handler]
321#[cfg(feature = "dev-oauth2-device-flow")]
322pub async fn view_device_get(
323    State(state): State<ServerState>,
324    Extension(_kopid): Extension<KOpId>,
325    VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
326    Query(user_code): Query<QueryUserCode>,
327) -> Result<Oauth2DeviceLoginView, (StatusCode, String)> {
328    Ok(Oauth2DeviceLoginView {
330        domain_custom_image: state.qe_r_ref.domain_info_read().has_custom_image(),
331        title: "Device Login".to_string(),
332        user_code: user_code.user_code.unwrap_or("".to_string()),
333    })
334}
335
336#[derive(Deserialize)]
337#[cfg(feature = "dev-oauth2-device-flow")]
338pub struct Oauth2DeviceLoginForm {
339    user_code: String,
340    confirm_login: bool,
341}
342
343#[cfg(feature = "dev-oauth2-device-flow")]
344#[axum::debug_handler]
345pub async fn view_device_post(
346    State(_state): State<ServerState>,
347    Extension(_kopid): Extension<KOpId>,
348    VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
349    Form(form): Form<Oauth2DeviceLoginForm>,
350) -> Result<String, (StatusCode, &'static str)> {
351    debug!("User code: {}", form.user_code);
352    debug!("User confirmed: {}", form.confirm_login);
353
354    Err((StatusCode::NOT_IMPLEMENTED, "Not implemented yet"))
357}