1use axum::extract::State;
2use axum::http::header::COOKIE;
3use axum::http::{HeaderMap, StatusCode};
4use axum::response::{IntoResponse, Response};
5use axum::{Form, Json};
6use serde::Deserialize;
7use serde_json::json;
8use url::Url;
9
10use allowthem_core::applications::{Application, BrandingConfig, validate_redirect_uri};
11use allowthem_core::authorization::{
12 generate_authorization_code, hash_authorization_code, validate_scopes,
13};
14use allowthem_core::types::{ClientId, UserId};
15use allowthem_core::{AllowThem, AuthError};
16
17enum OAuthErrorCode {
22 InvalidRequest,
23 AccessDenied,
24 UnsupportedResponseType,
25 InvalidScope,
26 ServerError,
27}
28
29impl OAuthErrorCode {
30 fn as_str(&self) -> &'static str {
31 match self {
32 Self::InvalidRequest => "invalid_request",
33 Self::AccessDenied => "access_denied",
34 Self::UnsupportedResponseType => "unsupported_response_type",
35 Self::InvalidScope => "invalid_scope",
36 Self::ServerError => "server_error",
37 }
38 }
39}
40
41pub enum AuthorizeOutcome {
48 Redirect(Response),
50 ConsentNeeded(Box<ConsentNeededData>),
52}
53
54pub struct ConsentNeededData {
55 pub context: ConsentContext,
56 pub params: ValidatedAuthorize,
57}
58
59#[derive(Deserialize)]
64pub struct AuthorizeParams {
65 pub client_id: Option<ClientId>,
66 pub redirect_uri: Option<String>,
67 pub response_type: Option<String>,
68 pub scope: Option<String>,
69 pub state: Option<String>,
70 pub code_challenge: Option<String>,
71 pub code_challenge_method: Option<String>,
72 pub nonce: Option<String>,
73}
74
75#[derive(Deserialize)]
77pub struct ConsentSubmission {
78 client_id: Option<ClientId>,
79 redirect_uri: Option<String>,
80 response_type: Option<String>,
81 scope: Option<String>,
82 state: Option<String>,
83 code_challenge: Option<String>,
84 code_challenge_method: Option<String>,
85 nonce: Option<String>,
86 consent: String,
87 #[allow(dead_code)]
89 csrf_token: Option<String>,
90}
91
92pub struct ConsentContext {
94 pub branding: BrandingConfig,
95 pub scopes: Vec<String>,
96}
97
98pub struct ValidatedAuthorize {
100 pub application: Application,
101 pub redirect_uri: String,
102 pub scopes: Vec<String>,
103 pub state: String,
104 pub code_challenge: String,
105 pub code_challenge_method: String,
106 pub nonce: Option<String>,
107}
108
109fn success_redirect(redirect_uri: &str, code: &str, state: &str, status: StatusCode) -> Response {
115 let mut url = Url::parse(redirect_uri).expect("redirect_uri was pre-validated");
116 url.query_pairs_mut()
117 .append_pair("code", code)
118 .append_pair("state", state);
119 (status, [("location", url.as_str().to_string())]).into_response()
120}
121
122fn error_redirect(
124 redirect_uri: &str,
125 error: OAuthErrorCode,
126 description: &str,
127 state: &str,
128 status: StatusCode,
129) -> Response {
130 let mut url = Url::parse(redirect_uri).expect("redirect_uri was pre-validated");
131 url.query_pairs_mut()
132 .append_pair("error", error.as_str())
133 .append_pair("error_description", description)
134 .append_pair("state", state);
135 (status, [("location", url.as_str().to_string())]).into_response()
136}
137
138fn display_error(status: StatusCode, message: &str) -> Response {
140 (status, Json(json!({"error": message}))).into_response()
141}
142
143pub async fn resolve_user(
151 ath: &AllowThem,
152 headers: &HeaderMap,
153) -> Result<Option<allowthem_core::User>, AuthError> {
154 let cookie_str = match headers.get(COOKIE).and_then(|v| v.to_str().ok()) {
155 Some(c) => c,
156 None => return Ok(None),
157 };
158
159 let token =
160 match allowthem_core::parse_session_cookie(cookie_str, ath.session_config().cookie_name) {
161 Some(t) => t,
162 None => return Ok(None),
163 };
164
165 let session = match ath
166 .db()
167 .validate_session(&token, ath.session_config().ttl)
168 .await?
169 {
170 Some(s) => s,
171 None => return Ok(None),
172 };
173
174 match ath.db().get_user(session.user_id).await {
175 Ok(user) if user.is_active => Ok(Some(user)),
176 Ok(_) => Ok(None),
177 Err(AuthError::NotFound) => Ok(None),
178 Err(e) => Err(e),
179 }
180}
181
182pub async fn validate_authorize_params(
189 ath: &AllowThem,
190 params: &AuthorizeParams,
191) -> Result<ValidatedAuthorize, Response> {
192 let client_id = params
194 .client_id
195 .as_ref()
196 .ok_or_else(|| display_error(StatusCode::BAD_REQUEST, "missing client_id"))?;
197
198 let application = ath
199 .db()
200 .get_application_by_client_id(client_id)
201 .await
202 .map_err(|e| match e {
203 AuthError::NotFound => display_error(StatusCode::BAD_REQUEST, "unknown client_id"),
204 _ => display_error(StatusCode::INTERNAL_SERVER_ERROR, "internal error"),
205 })?;
206
207 if !application.is_active {
209 return Err(display_error(
210 StatusCode::BAD_REQUEST,
211 "application is inactive",
212 ));
213 }
214
215 let redirect_uri = params.redirect_uri.as_deref().unwrap_or("");
217 if redirect_uri.is_empty() {
218 return Err(display_error(
219 StatusCode::BAD_REQUEST,
220 "missing redirect_uri",
221 ));
222 }
223 let registered = application
224 .redirect_uri_list()
225 .map_err(|_| display_error(StatusCode::INTERNAL_SERVER_ERROR, "internal error"))?;
226 validate_redirect_uri(redirect_uri, ®istered)
227 .map_err(|_| display_error(StatusCode::BAD_REQUEST, "redirect_uri not registered"))?;
228
229 let redirect_uri = redirect_uri.to_string();
231
232 let state = match params.state.as_deref() {
234 Some(s) if !s.is_empty() => s.to_string(),
235 _ => {
236 return Err(error_redirect(
237 &redirect_uri,
238 OAuthErrorCode::InvalidRequest,
239 "missing state parameter",
240 "",
241 StatusCode::FOUND,
242 ));
243 }
244 };
245
246 if params.response_type.as_deref() != Some("code") {
248 return Err(error_redirect(
249 &redirect_uri,
250 OAuthErrorCode::UnsupportedResponseType,
251 "response_type must be code",
252 &state,
253 StatusCode::FOUND,
254 ));
255 }
256
257 let scope_str = params.scope.as_deref().unwrap_or("");
259 let scopes = validate_scopes(scope_str).map_err(|e| {
260 error_redirect(
261 &redirect_uri,
262 OAuthErrorCode::InvalidScope,
263 &e.to_string(),
264 &state,
265 StatusCode::FOUND,
266 )
267 })?;
268
269 let code_challenge = match params.code_challenge.as_deref() {
271 Some(c) if !c.is_empty() => c.to_string(),
272 _ => {
273 return Err(error_redirect(
274 &redirect_uri,
275 OAuthErrorCode::InvalidRequest,
276 "missing code_challenge (PKCE required)",
277 &state,
278 StatusCode::FOUND,
279 ));
280 }
281 };
282 let code_challenge_method = params.code_challenge_method.as_deref().unwrap_or("");
283 if code_challenge_method != "S256" {
284 return Err(error_redirect(
285 &redirect_uri,
286 OAuthErrorCode::InvalidRequest,
287 "code_challenge_method must be S256",
288 &state,
289 StatusCode::FOUND,
290 ));
291 }
292
293 Ok(ValidatedAuthorize {
294 application,
295 redirect_uri,
296 scopes,
297 state,
298 code_challenge,
299 code_challenge_method: "S256".to_string(),
300 nonce: params.nonce.clone(),
301 })
302}
303
304fn build_authorize_query_string(params: &AuthorizeParams) -> String {
310 let mut pairs = url::form_urlencoded::Serializer::new(String::new());
311 if let Some(ref v) = params.client_id {
312 pairs.append_pair("client_id", v.as_str());
313 }
314 if let Some(ref v) = params.redirect_uri {
315 pairs.append_pair("redirect_uri", v);
316 }
317 if let Some(ref v) = params.response_type {
318 pairs.append_pair("response_type", v);
319 }
320 if let Some(ref v) = params.scope {
321 pairs.append_pair("scope", v);
322 }
323 if let Some(ref v) = params.state {
324 pairs.append_pair("state", v);
325 }
326 if let Some(ref v) = params.code_challenge {
327 pairs.append_pair("code_challenge", v);
328 }
329 if let Some(ref v) = params.code_challenge_method {
330 pairs.append_pair("code_challenge_method", v);
331 }
332 if let Some(ref v) = params.nonce {
333 pairs.append_pair("nonce", v);
334 }
335 pairs.finish()
336}
337
338fn login_redirect(params: &AuthorizeParams) -> Response {
340 let full_uri = format!("/oauth/authorize?{}", build_authorize_query_string(params));
341 let encoded: String = url::form_urlencoded::byte_serialize(full_uri.as_bytes()).collect();
342 let mut redirect = format!("/login?next={encoded}");
343 if let Some(ref cid) = params.client_id {
344 redirect.push_str("&client_id=");
345 redirect.push_str(cid.as_str());
346 }
347 (StatusCode::SEE_OTHER, [("location", redirect)]).into_response()
348}
349
350pub async fn issue_code_and_redirect(
352 ath: &AllowThem,
353 validated: &ValidatedAuthorize,
354 user_id: UserId,
355 status: StatusCode,
356) -> Response {
357 let raw_code = generate_authorization_code();
358 let code_hash = hash_authorization_code(&raw_code);
359
360 match ath
361 .db()
362 .create_authorization_code(
363 validated.application.id,
364 user_id,
365 &code_hash,
366 &validated.redirect_uri,
367 &validated.scopes,
368 &validated.code_challenge,
369 &validated.code_challenge_method,
370 validated.nonce.as_deref(),
371 )
372 .await
373 {
374 Ok(_) => success_redirect(&validated.redirect_uri, &raw_code, &validated.state, status),
375 Err(_) => error_redirect(
376 &validated.redirect_uri,
377 OAuthErrorCode::ServerError,
378 "internal error",
379 &validated.state,
380 status,
381 ),
382 }
383}
384
385pub async fn check_authorization(
392 ath: &AllowThem,
393 headers: &HeaderMap,
394 params: &AuthorizeParams,
395) -> AuthorizeOutcome {
396 let validated = match validate_authorize_params(ath, params).await {
397 Ok(v) => v,
398 Err(resp) => return AuthorizeOutcome::Redirect(resp),
399 };
400
401 let user = match resolve_user(ath, headers).await {
403 Ok(Some(u)) => u,
404 Ok(None) => return AuthorizeOutcome::Redirect(login_redirect(params)),
405 Err(_) => {
406 return AuthorizeOutcome::Redirect(error_redirect(
407 &validated.redirect_uri,
408 OAuthErrorCode::ServerError,
409 "internal error",
410 &validated.state,
411 StatusCode::FOUND,
412 ));
413 }
414 };
415
416 let needs_consent = if validated.application.is_trusted {
418 false
419 } else {
420 match ath
421 .db()
422 .has_sufficient_consent(user.id, validated.application.id, &validated.scopes)
423 .await
424 {
425 Ok(has) => !has,
426 Err(_) => {
427 return AuthorizeOutcome::Redirect(error_redirect(
428 &validated.redirect_uri,
429 OAuthErrorCode::ServerError,
430 "internal error",
431 &validated.state,
432 StatusCode::FOUND,
433 ));
434 }
435 }
436 };
437
438 if needs_consent {
439 let context = ConsentContext {
440 branding: validated.application.branding(),
441 scopes: validated.scopes.clone(),
442 };
443 return AuthorizeOutcome::ConsentNeeded(Box::new(ConsentNeededData {
444 context,
445 params: validated,
446 }));
447 }
448
449 AuthorizeOutcome::Redirect(
451 issue_code_and_redirect(ath, &validated, user.id, StatusCode::FOUND).await,
452 )
453}
454
455pub async fn authorize_post(
456 State(ath): State<AllowThem>,
457 headers: HeaderMap,
458 Form(form): Form<ConsentSubmission>,
459) -> Response {
460 let params = AuthorizeParams {
462 client_id: form.client_id,
463 redirect_uri: form.redirect_uri,
464 response_type: form.response_type,
465 scope: form.scope,
466 state: form.state,
467 code_challenge: form.code_challenge,
468 code_challenge_method: form.code_challenge_method,
469 nonce: form.nonce,
470 };
471 let validated = match validate_authorize_params(&ath, ¶ms).await {
472 Ok(v) => v,
473 Err(resp) => return resp,
474 };
475
476 let user = match resolve_user(&ath, &headers).await {
478 Ok(Some(u)) => u,
479 Ok(None) => return login_redirect(¶ms),
480 Err(_) => {
481 return error_redirect(
482 &validated.redirect_uri,
483 OAuthErrorCode::ServerError,
484 "internal error",
485 &validated.state,
486 StatusCode::SEE_OTHER,
487 );
488 }
489 };
490
491 if form.consent != "approve" {
493 return error_redirect(
494 &validated.redirect_uri,
495 OAuthErrorCode::AccessDenied,
496 "user denied consent",
497 &validated.state,
498 StatusCode::SEE_OTHER,
499 );
500 }
501
502 if ath
504 .db()
505 .upsert_consent(user.id, validated.application.id, &validated.scopes)
506 .await
507 .is_err()
508 {
509 return error_redirect(
510 &validated.redirect_uri,
511 OAuthErrorCode::ServerError,
512 "internal error",
513 &validated.state,
514 StatusCode::SEE_OTHER,
515 );
516 }
517
518 issue_code_and_redirect(&ath, &validated, user.id, StatusCode::SEE_OTHER).await
520}
521
522#[cfg(test)]
531mod tests {
532 use super::*;
533 use allowthem_core::handle::AllowThemBuilder;
534 use allowthem_core::types::Email;
535 use axum::Router;
536 use axum::body::Body;
537 use axum::http::Request;
538 use axum::routing::post;
539 use tower::ServiceExt;
540
541 async fn test_ath() -> AllowThem {
542 AllowThemBuilder::new("sqlite::memory:")
543 .cookie_secure(false)
544 .build()
545 .await
546 .unwrap()
547 }
548
549 async fn setup_application(ath: &AllowThem) -> Application {
550 let email = Email::new("admin@example.com".into()).unwrap();
551 let user = ath
552 .db()
553 .create_user(email, "password123", None, None)
554 .await
555 .unwrap();
556
557 let (app, _) = ath
558 .db()
559 .create_application(
560 "TestApp".to_string(),
561 vec!["https://example.com/callback".to_string()],
562 false,
563 Some(user.id),
564 None,
565 None,
566 )
567 .await
568 .unwrap();
569 app
570 }
571
572 fn authorize_params(app: &Application) -> AuthorizeParams {
573 AuthorizeParams {
574 client_id: Some(app.client_id.clone()),
575 redirect_uri: Some("https://example.com/callback".into()),
576 response_type: Some("code".into()),
577 scope: Some("openid profile".into()),
578 state: Some("xyz".into()),
579 code_challenge: Some("abc123".into()),
580 code_challenge_method: Some("S256".into()),
581 nonce: None,
582 }
583 }
584
585 fn expect_redirect(outcome: AuthorizeOutcome) -> Response {
587 match outcome {
588 AuthorizeOutcome::Redirect(resp) => resp,
589 AuthorizeOutcome::ConsentNeeded(_) => {
590 panic!("expected Redirect, got ConsentNeeded")
591 }
592 }
593 }
594
595 async fn read_body(resp: axum::http::Response<Body>) -> serde_json::Value {
596 let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
597 .await
598 .unwrap();
599 serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null)
600 }
601
602 async fn create_session(
604 ath: &AllowThem,
605 email: &str,
606 ) -> (allowthem_core::types::UserId, String) {
607 let email = Email::new(email.into()).unwrap();
608 let user = ath
609 .db()
610 .create_user(email, "password123", None, None)
611 .await
612 .unwrap();
613 let token = allowthem_core::generate_token();
614 let hash = allowthem_core::hash_token(&token);
615 let expires = chrono::Utc::now() + chrono::Duration::hours(24);
616 ath.db()
617 .create_session(user.id, hash, None, None, expires)
618 .await
619 .unwrap();
620 let cookie = format!("allowthem_session={}", token.as_str());
621 (user.id, cookie)
622 }
623
624 fn headers_with_cookie(cookie: &str) -> HeaderMap {
625 let mut headers = HeaderMap::new();
626 headers.insert("cookie", cookie.parse().unwrap());
627 headers
628 }
629
630 #[tokio::test]
633 async fn missing_client_id_returns_400() {
634 let ath = test_ath().await;
635 let params = AuthorizeParams {
636 client_id: None,
637 redirect_uri: Some("x".into()),
638 response_type: Some("code".into()),
639 scope: Some("openid".into()),
640 state: Some("s".into()),
641 code_challenge: Some("c".into()),
642 code_challenge_method: Some("S256".into()),
643 nonce: None,
644 };
645 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
646 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
647 let body = read_body(resp).await;
648 assert_eq!(body["error"], "missing client_id");
649 }
650
651 #[tokio::test]
652 async fn unknown_client_id_returns_400() {
653 let ath = test_ath().await;
654 let params = AuthorizeParams {
655 client_id: serde_json::from_value(serde_json::json!("ath_nonexistent")).ok(),
656 redirect_uri: Some("x".into()),
657 response_type: Some("code".into()),
658 scope: Some("openid".into()),
659 state: Some("s".into()),
660 code_challenge: Some("c".into()),
661 code_challenge_method: Some("S256".into()),
662 nonce: None,
663 };
664 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
665 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
666 let body = read_body(resp).await;
667 assert_eq!(body["error"], "unknown client_id");
668 }
669
670 #[tokio::test]
671 async fn unregistered_redirect_uri_returns_400() {
672 let ath = test_ath().await;
673 let application = setup_application(&ath).await;
674 let params = AuthorizeParams {
675 client_id: Some(application.client_id.clone()),
676 redirect_uri: Some("https://evil.example.com/callback".into()),
677 response_type: Some("code".into()),
678 scope: Some("openid".into()),
679 state: Some("s".into()),
680 code_challenge: Some("c".into()),
681 code_challenge_method: Some("S256".into()),
682 nonce: None,
683 };
684 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
685 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
686 let body = read_body(resp).await;
687 assert_eq!(body["error"], "redirect_uri not registered");
688 }
689
690 #[tokio::test]
693 async fn missing_state_redirects_with_error() {
694 let ath = test_ath().await;
695 let application = setup_application(&ath).await;
696 let params = AuthorizeParams {
697 client_id: Some(application.client_id.clone()),
698 redirect_uri: Some("https://example.com/callback".into()),
699 response_type: Some("code".into()),
700 scope: Some("openid".into()),
701 state: None,
702 code_challenge: Some("c".into()),
703 code_challenge_method: Some("S256".into()),
704 nonce: None,
705 };
706 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
707 assert_eq!(resp.status(), StatusCode::FOUND);
708 let location = resp.headers().get("location").unwrap().to_str().unwrap();
709 assert!(location.contains("error=invalid_request"));
710 }
711
712 #[tokio::test]
713 async fn bad_response_type_redirects_with_error() {
714 let ath = test_ath().await;
715 let application = setup_application(&ath).await;
716 let params = AuthorizeParams {
717 client_id: Some(application.client_id.clone()),
718 redirect_uri: Some("https://example.com/callback".into()),
719 response_type: Some("token".into()),
720 scope: Some("openid".into()),
721 state: Some("s".into()),
722 code_challenge: Some("c".into()),
723 code_challenge_method: Some("S256".into()),
724 nonce: None,
725 };
726 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
727 assert_eq!(resp.status(), StatusCode::FOUND);
728 let location = resp.headers().get("location").unwrap().to_str().unwrap();
729 assert!(location.contains("error=unsupported_response_type"));
730 assert!(location.contains("state=s"));
731 }
732
733 #[tokio::test]
734 async fn invalid_scope_redirects_with_error() {
735 let ath = test_ath().await;
736 let application = setup_application(&ath).await;
737 let params = AuthorizeParams {
738 client_id: Some(application.client_id.clone()),
739 redirect_uri: Some("https://example.com/callback".into()),
740 response_type: Some("code".into()),
741 scope: Some("profile".into()),
742 state: Some("s".into()),
743 code_challenge: Some("c".into()),
744 code_challenge_method: Some("S256".into()),
745 nonce: None,
746 };
747 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
748 assert_eq!(resp.status(), StatusCode::FOUND);
749 let location = resp.headers().get("location").unwrap().to_str().unwrap();
750 assert!(location.contains("error=invalid_scope"));
751 }
752
753 #[tokio::test]
754 async fn missing_pkce_redirects_with_error() {
755 let ath = test_ath().await;
756 let application = setup_application(&ath).await;
757 let params = AuthorizeParams {
758 client_id: Some(application.client_id.clone()),
759 redirect_uri: Some("https://example.com/callback".into()),
760 response_type: Some("code".into()),
761 scope: Some("openid".into()),
762 state: Some("s".into()),
763 code_challenge: None,
764 code_challenge_method: None,
765 nonce: None,
766 };
767 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
768 assert_eq!(resp.status(), StatusCode::FOUND);
769 let location = resp.headers().get("location").unwrap().to_str().unwrap();
770 assert!(location.contains("error=invalid_request"));
771 assert!(location.contains("PKCE"));
772 }
773
774 #[tokio::test]
777 async fn unauthenticated_redirects_to_login() {
778 let ath = test_ath().await;
779 let application = setup_application(&ath).await;
780 let params = authorize_params(&application);
781 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
782 assert_eq!(resp.status(), StatusCode::SEE_OTHER);
783 let location = resp.headers().get("location").unwrap().to_str().unwrap();
784 assert!(location.starts_with("/login?next="));
785 assert!(location.contains("oauth%2Fauthorize"));
786 }
787
788 #[tokio::test]
791 async fn trusted_app_skips_consent_and_redirects_with_code() {
792 let ath = test_ath().await;
793 let (_, cookie) = create_session(&ath, "trusted@example.com").await;
794 let headers = headers_with_cookie(&cookie);
795
796 let (trusted_app, _) = ath
797 .db()
798 .create_application(
799 "TrustedApp".to_string(),
800 vec!["https://trusted.example.com/callback".to_string()],
801 true,
802 None,
803 None,
804 None,
805 )
806 .await
807 .unwrap();
808
809 let params = AuthorizeParams {
810 client_id: Some(trusted_app.client_id.clone()),
811 redirect_uri: Some("https://trusted.example.com/callback".into()),
812 response_type: Some("code".into()),
813 scope: Some("openid profile".into()),
814 state: Some("xyz".into()),
815 code_challenge: Some("abc123".into()),
816 code_challenge_method: Some("S256".into()),
817 nonce: None,
818 };
819
820 let resp = expect_redirect(check_authorization(&ath, &headers, ¶ms).await);
821 assert_eq!(resp.status(), StatusCode::FOUND);
822 let location = resp.headers().get("location").unwrap().to_str().unwrap();
823 assert!(location.contains("code="));
824 assert!(location.contains("state=xyz"));
825 assert!(location.starts_with("https://trusted.example.com/callback"));
826 }
827
828 #[tokio::test]
831 async fn untrusted_app_without_consent_returns_consent_needed() {
832 let ath = test_ath().await;
833 let (_, cookie) = create_session(&ath, "consent@example.com").await;
834 let headers = headers_with_cookie(&cookie);
835 let application = setup_application(&ath).await;
836 let params = authorize_params(&application);
837
838 let outcome = check_authorization(&ath, &headers, ¶ms).await;
839 match outcome {
840 AuthorizeOutcome::ConsentNeeded(data) => {
841 assert_eq!(data.context.branding.application_name, "TestApp");
842 assert_eq!(data.context.scopes, vec!["openid", "profile"]);
843 }
844 AuthorizeOutcome::Redirect(_) => panic!("expected ConsentNeeded, got Redirect"),
845 }
846 }
847
848 #[tokio::test]
851 async fn inactive_application_returns_400() {
852 let ath = test_ath().await;
853 let application = setup_application(&ath).await;
854
855 sqlx::query("UPDATE allowthem_applications SET is_active = 0 WHERE id = ?")
856 .bind(application.id)
857 .execute(ath.db().pool())
858 .await
859 .unwrap();
860
861 let params = AuthorizeParams {
862 client_id: Some(application.client_id.clone()),
863 redirect_uri: Some("https://example.com/callback".into()),
864 response_type: Some("code".into()),
865 scope: Some("openid".into()),
866 state: Some("s".into()),
867 code_challenge: Some("c".into()),
868 code_challenge_method: Some("S256".into()),
869 nonce: None,
870 };
871 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
872 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
873 let body = read_body(resp).await;
874 assert_eq!(body["error"], "application is inactive");
875 }
876
877 #[tokio::test]
880 async fn wrong_pkce_method_redirects_with_error() {
881 let ath = test_ath().await;
882 let application = setup_application(&ath).await;
883 let params = AuthorizeParams {
884 client_id: Some(application.client_id.clone()),
885 redirect_uri: Some("https://example.com/callback".into()),
886 response_type: Some("code".into()),
887 scope: Some("openid".into()),
888 state: Some("s".into()),
889 code_challenge: Some("c".into()),
890 code_challenge_method: Some("plain".into()),
891 nonce: None,
892 };
893 let resp = expect_redirect(check_authorization(&ath, &HeaderMap::new(), ¶ms).await);
894 assert_eq!(resp.status(), StatusCode::FOUND);
895 let location = resp.headers().get("location").unwrap().to_str().unwrap();
896 assert!(location.contains("error=invalid_request"));
897 assert!(location.contains("state=s"));
898 }
899
900 #[tokio::test]
903 async fn existing_consent_skips_consent_screen() {
904 let ath = test_ath().await;
905 let (user_id, cookie) = create_session(&ath, "existing_consent@example.com").await;
906 let headers = headers_with_cookie(&cookie);
907 let application = setup_application(&ath).await;
908
909 ath.db()
910 .upsert_consent(
911 user_id,
912 application.id,
913 &["openid".to_string(), "profile".to_string()],
914 )
915 .await
916 .unwrap();
917
918 let params = authorize_params(&application);
919 let resp = expect_redirect(check_authorization(&ath, &headers, ¶ms).await);
920 assert_eq!(resp.status(), StatusCode::FOUND);
921 let location = resp.headers().get("location").unwrap().to_str().unwrap();
922 assert!(location.contains("code="));
923 assert!(location.contains("state=xyz"));
924 }
925
926 fn post_app(ath: AllowThem) -> Router {
929 Router::new()
930 .route("/oauth/authorize", post(authorize_post))
931 .with_state(ath)
932 }
933
934 #[tokio::test]
935 async fn post_approve_creates_code_and_redirects_303() {
936 let ath = test_ath().await;
937 let app = post_app(ath.clone());
938 let (_, cookie) = create_session(&ath, "post_approve@example.com").await;
939 let application = setup_application(&ath).await;
940
941 let body = url::form_urlencoded::Serializer::new(String::new())
942 .append_pair("client_id", application.client_id.as_str())
943 .append_pair("redirect_uri", "https://example.com/callback")
944 .append_pair("response_type", "code")
945 .append_pair("scope", "openid profile")
946 .append_pair("state", "mystate")
947 .append_pair("code_challenge", "mychallenge")
948 .append_pair("code_challenge_method", "S256")
949 .append_pair("consent", "approve")
950 .finish();
951
952 let req = Request::builder()
953 .method("POST")
954 .uri("/oauth/authorize")
955 .header("cookie", &cookie)
956 .header("content-type", "application/x-www-form-urlencoded")
957 .body(Body::from(body))
958 .unwrap();
959 let resp = app.oneshot(req).await.unwrap();
960 assert_eq!(resp.status(), StatusCode::SEE_OTHER);
961 let location = resp.headers().get("location").unwrap().to_str().unwrap();
962 assert!(location.starts_with("https://example.com/callback"));
963 assert!(location.contains("code="));
964 assert!(location.contains("state=mystate"));
965 }
966
967 #[tokio::test]
968 async fn post_deny_redirects_with_access_denied_303() {
969 let ath = test_ath().await;
970 let app = post_app(ath.clone());
971 let (_, cookie) = create_session(&ath, "post_deny@example.com").await;
972 let application = setup_application(&ath).await;
973
974 let body = url::form_urlencoded::Serializer::new(String::new())
975 .append_pair("client_id", application.client_id.as_str())
976 .append_pair("redirect_uri", "https://example.com/callback")
977 .append_pair("response_type", "code")
978 .append_pair("scope", "openid profile")
979 .append_pair("state", "mystate")
980 .append_pair("code_challenge", "mychallenge")
981 .append_pair("code_challenge_method", "S256")
982 .append_pair("consent", "deny")
983 .finish();
984
985 let req = Request::builder()
986 .method("POST")
987 .uri("/oauth/authorize")
988 .header("cookie", &cookie)
989 .header("content-type", "application/x-www-form-urlencoded")
990 .body(Body::from(body))
991 .unwrap();
992 let resp = app.oneshot(req).await.unwrap();
993 assert_eq!(resp.status(), StatusCode::SEE_OTHER);
994 let location = resp.headers().get("location").unwrap().to_str().unwrap();
995 assert!(location.contains("error=access_denied"));
996 assert!(location.contains("state=mystate"));
997 }
998
999 #[tokio::test]
1000 async fn post_unauthenticated_redirects_to_login() {
1001 let ath = test_ath().await;
1002 let app = post_app(ath.clone());
1003 let application = setup_application(&ath).await;
1004
1005 let body = url::form_urlencoded::Serializer::new(String::new())
1006 .append_pair("client_id", application.client_id.as_str())
1007 .append_pair("redirect_uri", "https://example.com/callback")
1008 .append_pair("response_type", "code")
1009 .append_pair("scope", "openid")
1010 .append_pair("state", "s")
1011 .append_pair("code_challenge", "c")
1012 .append_pair("code_challenge_method", "S256")
1013 .append_pair("consent", "approve")
1014 .finish();
1015
1016 let req = Request::builder()
1017 .method("POST")
1018 .uri("/oauth/authorize")
1019 .header("content-type", "application/x-www-form-urlencoded")
1020 .body(Body::from(body))
1021 .unwrap();
1022 let resp = app.oneshot(req).await.unwrap();
1023 assert_eq!(resp.status(), StatusCode::SEE_OTHER);
1024 let location = resp.headers().get("location").unwrap().to_str().unwrap();
1025 assert!(location.starts_with("/login?next="));
1026 }
1027
1028 #[tokio::test]
1029 async fn post_with_invalid_client_id_returns_400() {
1030 let ath = test_ath().await;
1031 let app = post_app(ath.clone());
1032 let (_, cookie) = create_session(&ath, "post_revalidate@example.com").await;
1033
1034 let body = url::form_urlencoded::Serializer::new(String::new())
1035 .append_pair("client_id", "ath_nonexistent")
1036 .append_pair("redirect_uri", "https://example.com/callback")
1037 .append_pair("response_type", "code")
1038 .append_pair("scope", "openid")
1039 .append_pair("state", "s")
1040 .append_pair("code_challenge", "c")
1041 .append_pair("code_challenge_method", "S256")
1042 .append_pair("consent", "approve")
1043 .finish();
1044
1045 let req = Request::builder()
1046 .method("POST")
1047 .uri("/oauth/authorize")
1048 .header("cookie", &cookie)
1049 .header("content-type", "application/x-www-form-urlencoded")
1050 .body(Body::from(body))
1051 .unwrap();
1052 let resp = app.oneshot(req).await.unwrap();
1053 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1054 let body = read_body(resp).await;
1055 assert_eq!(body["error"], "unknown client_id");
1056 }
1057}