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