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}