use crate::https::{
extractors::{DomainInfo, DomainInfoRead, VerifiedClientInformation},
middleware::KOpId,
ServerState,
};
use kanidmd_lib::idm::oauth2::{
AuthorisationRequest, AuthorisePermitSuccess, AuthoriseResponse, Oauth2Error,
};
use kanidmd_lib::prelude::*;
use kanidm_proto::internal::COOKIE_OAUTH2_REQ;
use std::collections::BTreeSet;
use askama::Template;
#[cfg(feature = "dev-oauth2-device-flow")]
use axum::http::StatusCode;
use axum::{
extract::{Query, State},
http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
response::{IntoResponse, Redirect, Response},
Extension, Form,
};
use axum_extra::extract::cookie::{CookieJar, SameSite};
use axum_htmx::HX_REDIRECT;
use serde::Deserialize;
use super::login::{LoginDisplayCtx, Oauth2Ctx};
use super::{cookies, UnrecoverableErrorView};
#[derive(Template)]
#[template(path = "oauth2_consent_request.html")]
struct ConsentRequestView {
client_name: String,
pii_scopes: BTreeSet<String>,
consent_token: String,
redirect: Option<String>,
}
#[derive(Template)]
#[template(path = "oauth2_access_denied.html")]
struct AccessDeniedView {
operation_id: Uuid,
}
pub async fn view_index_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar,
Query(auth_req): Query<AuthorisationRequest>,
) -> Response {
oauth2_auth_req(
state,
kopid,
client_auth_info,
domain_info,
jar,
Some(auth_req),
)
.await
}
pub async fn view_resume_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
DomainInfo(domain_info): DomainInfo,
jar: CookieJar,
) -> Response {
let maybe_auth_req =
cookies::get_signed::<AuthorisationRequest>(&state, &jar, COOKIE_OAUTH2_REQ);
oauth2_auth_req(
state,
kopid,
client_auth_info,
domain_info,
jar,
maybe_auth_req,
)
.await
}
async fn oauth2_auth_req(
state: ServerState,
kopid: KOpId,
client_auth_info: ClientAuthInfo,
domain_info: DomainInfoRead,
jar: CookieJar,
maybe_auth_req: Option<AuthorisationRequest>,
) -> Response {
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &state);
let Some(auth_req) = maybe_auth_req else {
error!("unable to resume session, no valid auth_req was found in the cookie. This cookie has been removed.");
return (
jar,
UnrecoverableErrorView {
err_code: OperationError::UI0003InvalidOauth2Resume,
operation_id: kopid.eventid,
},
)
.into_response();
};
let res: Result<AuthoriseResponse, Oauth2Error> = state
.qe_r_ref
.handle_oauth2_authorise(client_auth_info, auth_req.clone(), kopid.eventid)
.await;
match res {
Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
})) => {
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
(
jar,
[
(HX_REDIRECT, redirect_uri.as_str().to_string()),
(
ACCESS_CONTROL_ALLOW_ORIGIN.as_str(),
redirect_uri.origin().ascii_serialization(),
),
],
Redirect::to(redirect_uri.as_str()),
)
.into_response()
}
Ok(AuthoriseResponse::ConsentRequested {
client_name,
scopes: _,
pii_scopes,
consent_token,
}) => {
(
jar,
ConsentRequestView {
client_name,
pii_scopes,
consent_token,
redirect: None,
},
)
.into_response()
}
Ok(AuthoriseResponse::AuthenticationRequired {
client_name,
login_hint,
}) => {
let maybe_jar = cookies::make_signed(&state, COOKIE_OAUTH2_REQ, &auth_req)
.map(|mut cookie| {
cookie.set_same_site(SameSite::Strict);
cookie.set_expires(None);
cookie.set_max_age(time::Duration::minutes(15));
jar.clone().add(cookie)
})
.ok_or(OperationError::InvalidSessionState);
match maybe_jar {
Ok(new_jar) => {
let display_ctx = LoginDisplayCtx {
domain_info,
oauth2: Some(Oauth2Ctx { client_name }),
reauth: None,
error: None,
};
super::login::view_oauth2_get(new_jar, display_ctx, login_hint)
}
Err(err_code) => (
jar,
UnrecoverableErrorView {
err_code,
operation_id: kopid.eventid,
},
)
.into_response(),
}
}
Err(Oauth2Error::AccessDenied) => {
(
jar,
AccessDeniedView {
operation_id: kopid.eventid,
},
)
.into_response()
}
Err(err_code) => {
error!(
"Unable to authorise - Error ID: {:?} error: {}",
kopid.eventid,
&err_code.to_string()
);
(
jar,
UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
},
)
.into_response()
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConsentForm {
consent_token: String,
#[serde(default)]
#[allow(dead_code)] redirect: Option<String>,
}
pub async fn view_consent_post(
State(server_state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
jar: CookieJar,
Form(consent_form): Form<ConsentForm>,
) -> Result<Response, UnrecoverableErrorView> {
let res = server_state
.qe_w_ref
.handle_oauth2_authorise_permit(client_auth_info, consent_form.consent_token, kopid.eventid)
.await;
match res {
Ok(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
}) => {
let jar = cookies::destroy(jar, COOKIE_OAUTH2_REQ, &server_state);
if let Some(redirect) = consent_form.redirect {
Ok((
jar,
[
(HX_REDIRECT, redirect_uri.as_str().to_string()),
(
ACCESS_CONTROL_ALLOW_ORIGIN.as_str(),
redirect_uri.origin().ascii_serialization(),
),
],
Redirect::to(&redirect),
)
.into_response())
} else {
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
Ok((
jar,
[
(HX_REDIRECT, redirect_uri.as_str().to_string()),
(
ACCESS_CONTROL_ALLOW_ORIGIN.as_str(),
redirect_uri.origin().ascii_serialization(),
),
],
Redirect::to(redirect_uri.as_str()),
)
.into_response())
}
}
Err(err_code) => {
error!(
"Unable to authorise - Error ID: {:?} error: {}",
kopid.eventid,
&err_code.to_string()
);
Err(UnrecoverableErrorView {
err_code: OperationError::InvalidState,
operation_id: kopid.eventid,
})
}
}
}
#[derive(Template, Debug, Clone)]
#[cfg(feature = "dev-oauth2-device-flow")]
#[template(path = "oauth2_device_login.html")]
pub struct Oauth2DeviceLoginView {
domain_custom_image: bool,
title: String,
user_code: String,
}
#[derive(Debug, Clone, Deserialize)]
#[cfg(feature = "dev-oauth2-device-flow")]
pub(crate) struct QueryUserCode {
pub user_code: Option<String>,
}
#[axum::debug_handler]
#[cfg(feature = "dev-oauth2-device-flow")]
pub async fn view_device_get(
State(state): State<ServerState>,
Extension(_kopid): Extension<KOpId>,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
Query(user_code): Query<QueryUserCode>,
) -> Result<Oauth2DeviceLoginView, (StatusCode, String)> {
Ok(Oauth2DeviceLoginView {
domain_custom_image: state.qe_r_ref.domain_info_read().has_custom_image(),
title: "Device Login".to_string(),
user_code: user_code.user_code.unwrap_or("".to_string()),
})
}
#[derive(Deserialize)]
#[cfg(feature = "dev-oauth2-device-flow")]
pub struct Oauth2DeviceLoginForm {
user_code: String,
confirm_login: bool,
}
#[cfg(feature = "dev-oauth2-device-flow")]
#[axum::debug_handler]
pub async fn view_device_post(
State(_state): State<ServerState>,
Extension(_kopid): Extension<KOpId>,
VerifiedClientInformation(_client_auth_info): VerifiedClientInformation,
Form(form): Form<Oauth2DeviceLoginForm>,
) -> Result<String, (StatusCode, &'static str)> {
debug!("User code: {}", form.user_code);
debug!("User confirmed: {}", form.confirm_login);
Err((StatusCode::NOT_IMPLEMENTED, "Not implemented yet"))
}