1use super::*;
2use crate::landing;
3use didbot_brand::page::{escape, Landing, Tone};
4
5use crate::oauth::error::OAuthError;
6use crate::refusal::Severity;
7
8pub(crate) struct Refused {
11 error: OAuthError,
12 severity: Severity,
13 reason: Option<String>,
17}
18
19impl Refused {
20 pub(crate) fn info(error: OAuthError) -> Self {
21 Self {
22 error,
23 severity: Severity::Info,
24 reason: None,
25 }
26 }
27
28 pub(crate) fn warn(error: OAuthError) -> Self {
29 Self {
30 error,
31 severity: Severity::Warn,
32 reason: None,
33 }
34 }
35
36 fn because(mut self, reason: impl std::fmt::Display) -> Self {
37 self.reason = Some(reason.to_string());
38 self
39 }
40
41 pub(crate) fn answer(
43 self,
44 endpoint: &'static str,
45 client_id: Option<&str>,
46 account: Option<&str>,
47 ) -> Response {
48 self.write(endpoint, client_id, account);
49 self.error.into_response()
50 }
51
52 pub(crate) fn write(
58 &self,
59 endpoint: &'static str,
60 client_id: Option<&str>,
61 account: Option<&str>,
62 ) {
63 use crate::oauth::error::OAuthErrorCode;
64 if self.error.error() != OAuthErrorCode::UseDpopNonce.as_str() {
65 crate::refusal::refused(crate::refusal::Refusal {
66 endpoint,
67 client_id,
68 account,
69 error: self.error.error(),
70 reason: self
71 .reason
72 .as_deref()
73 .unwrap_or_else(|| self.error.description()),
74 severity: self.severity,
75 });
76 }
77 }
78}
79
80pub(crate) async fn oauth_server_metadata(State(state): State<AppState>) -> Response {
85 use crate::oauth::error::{OAuthError, OAuthErrorCode};
86 match crate::oauth::discovery::server_metadata(state.registry.as_ref()) {
87 Ok(metadata) => Json(metadata).into_response(),
88 Err(err) => OAuthError::new(OAuthErrorCode::ServerError, err.to_string()).into_response(),
89 }
90}
91
92pub(crate) async fn oauth_protected_resource_metadata(State(state): State<AppState>) -> Response {
97 use crate::oauth::error::{OAuthError, OAuthErrorCode};
98 match crate::oauth::discovery::protected_resource_metadata(state.registry.as_ref()) {
99 Ok(metadata) => Json(metadata).into_response(),
100 Err(err) => OAuthError::new(OAuthErrorCode::ServerError, err.to_string()).into_response(),
101 }
102}
103
104#[derive(serde::Deserialize)]
106pub(crate) struct OAuthParForm {
107 client_id: String,
108 response_type: Option<String>,
109 response_mode: Option<String>,
110 redirect_uri: Option<String>,
111 scope: Option<String>,
112 state: Option<String>,
113 code_challenge: Option<String>,
114 code_challenge_method: Option<String>,
115 login_hint: Option<String>,
116 client_assertion_type: Option<String>,
117 client_assertion: Option<String>,
118 dpop_jkt: Option<String>,
119}
120
121pub(crate) async fn oauth_par(
135 State(state): State<AppState>,
136 peer: Peer,
137 headers: HeaderMap,
138 axum::Form(form): axum::Form<OAuthParForm>,
139) -> Response {
140 use crate::oauth::par::{push_authorization_request, DecisionSeams, ParRequest};
141
142 let client_id = form.client_id.clone();
143 let refuse = |refused: Refused| refused.answer("par", Some(&client_id), None);
144
145 let key = caller_key(&headers, peer.0, state.trust_forwarded_headers);
148 if !state.oauth.par_rate_limiter.check(&key) {
149 return refuse(Refused::info(too_many_oauth_requests("par")));
150 }
151
152 let proof = match checked_proof(&state, &headers, "/oauth/par") {
156 Ok(proof) => proof,
157 Err(refused) => return refuse(Refused::info(refused)),
158 };
159
160 let request = ParRequest {
161 client_id: form.client_id,
162 response_type: form.response_type,
163 response_mode: form.response_mode,
164 redirect_uri: form.redirect_uri,
165 scope: form.scope,
166 state: form.state,
167 code_challenge: form.code_challenge,
168 code_challenge_method: form.code_challenge_method,
169 login_hint: form.login_hint,
170 credentials: crate::oauth::client_auth::ClientCredentials {
171 assertion_type: form.client_assertion_type,
172 assertion: form.client_assertion,
173 },
174 dpop_jkt: form.dpop_jkt,
175 proof_key: proof.thumbprint,
176 };
177 let issuer = crate::oauth::discovery::issuer_for(state.registry.as_ref()).unwrap_or_default();
180 let pds_did = deployment_did(&state);
181 let pds_hostname = state.registry.zone().to_owned();
182 let seams = DecisionSeams {
183 scope_policy: state.oauth.scope_policy.as_ref(),
184 consent_store: state.oauth.consent_store.as_ref(),
185 decisions: state.oauth.decision_store.as_ref(),
186 gate: state.oauth.grant_gate.as_ref(),
187 evaluation_log: state.oauth.evaluation_log.as_ref(),
188 pds_did: &pds_did,
189 pds_hostname: &pds_hostname,
190 loopback_clients: crate::oauth::client_metadata::LoopbackClients::for_zone(&pds_hostname),
191 client_assertions: state.oauth.client_assertions.as_ref(),
192 issuer: &issuer,
193 permission_sets: state.oauth.permission_sets.as_ref(),
194 };
195 match push_authorization_request(
196 &state.oauth.http,
197 state.oauth.client_cache.as_ref(),
198 state.oauth.par_store.as_ref(),
199 request,
200 &|hint| resolve_identifier(&state, hint),
201 &seams,
202 )
203 .await
204 {
205 Ok((request_uri, ttl)) => (
206 StatusCode::CREATED,
207 Json(json!({ "request_uri": request_uri, "expires_in": ttl.whole_seconds() })),
208 )
209 .into_response(),
210 Err(err) => refuse(par_refusal(err)),
211 }
212}
213
214fn par_refusal(err: crate::oauth::par::ParError) -> Refused {
220 use crate::oauth::error::OAuthErrorCode;
221 use crate::oauth::par::ParError;
222 let code = match err {
223 ParError::MissingParameter(_)
224 | ParError::BadScope(_)
225 | ParError::UnsupportedCodeChallengeMethod(_)
226 | ParError::MalformedCodeChallenge
227 | ParError::TooLong { .. }
228 | ParError::ClientRefused
229 | ParError::UnsupportedResponseMode(_)
230 | ParError::RedirectUriNotRegistered(_) => OAuthErrorCode::InvalidRequest,
231 ParError::UnsupportedResponseType => OAuthErrorCode::UnsupportedResponseType,
232 ParError::ClientId(_) | ParError::ClientMetadata(_) => OAuthErrorCode::InvalidClient,
233 ParError::ClientAuthentication(ref refused) => refused.code(),
234 ParError::DpopKeyMismatch => OAuthErrorCode::InvalidDpopProof,
235 ParError::TooManyPending(_) => OAuthErrorCode::AccessDenied,
236 };
237 let error = OAuthError::new(code, err.to_string());
238 match err {
239 ParError::ClientMetadata(cause) => Refused::warn(error).because(cause),
241 ParError::ClientAuthentication(_)
242 | ParError::RedirectUriNotRegistered(_)
243 | ParError::TooManyPending(_) => Refused::warn(error),
244 _ => Refused::info(error),
245 }
246}
247
248pub(crate) fn deployment_did(state: &AppState) -> String {
251 state
252 .registry
253 .did_document(state.registry.zone())
254 .map(|document| document.id.clone())
255 .unwrap_or_default()
256}
257
258#[derive(serde::Deserialize)]
260pub(crate) struct OAuthAuthorizeQuery {
261 client_id: String,
262 request_uri: String,
263}
264
265pub(crate) fn too_many_oauth_requests(endpoint: &str) -> OAuthError {
268 OAuthError::rate_limited(endpoint)
269}
270
271pub(crate) fn authorize_error(err: crate::oauth::authorize::AuthorizeError) -> OAuthError {
272 use crate::oauth::authorize::AuthorizeError;
273 use crate::oauth::error::OAuthErrorCode;
274 let code = match err {
275 AuthorizeError::UnknownRequestUri | AuthorizeError::ClientMismatch => {
276 OAuthErrorCode::InvalidGrant
277 }
278 };
279 OAuthError::new(code, err.to_string())
280}
281
282pub(crate) fn authorize_error_page(err: crate::oauth::authorize::AuthorizeError) -> Response {
285 use crate::oauth::authorize::AuthorizeError;
286 let what = match err {
287 AuthorizeError::UnknownRequestUri => "this sign-in request expired or is unknown",
288 AuthorizeError::ClientMismatch => "this link does not match its sign-in request",
289 };
290 landing::refused(
291 StatusCode::BAD_REQUEST,
292 landing::AUTHORIZE,
293 "invalid_grant",
294 what,
295 "",
296 "Start the sign-in again from the app.",
297 )
298}
299
300pub(crate) fn authorize_page(
342 pending: &crate::oauth::authorize::AuthorizePending,
343 authorize_url: &str,
344 sign_in_url: &str,
345) -> Response {
346 let record = &pending.record;
347 let items = |scopes: &crate::oauth::scope::ScopeSet| -> String {
348 let items: String = scopes
349 .0
350 .iter()
351 .map(|atom| {
352 let atom = escape(&atom.to_string());
353 format!("<li data-scope=\"{atom}\">{atom}</li>")
354 })
355 .collect();
356 format!("<ul>{items}</ul>")
357 };
358 let only = if pending.cut.0.is_empty() {
363 ""
364 } else {
365 "only "
366 };
367 let allowed = format!(
368 "The scope ceiling allows {only}these scopes:{}",
369 items(&pending.granted)
370 );
371 let account = escape(&record.account);
372 let browser = match (pending.signed_in, pending.held) {
373 (true, _) => {
374 format!("<p data-browser=\"signed-in\">This browser is signed in as {account}.</p>")
375 }
376 (false, false) => format!(
377 "<p data-browser=\"not-signed-in\">This browser is not signed in as {account}.</p>"
378 ),
379 (false, true) => format!(
380 "<p class=\"next\" data-browser=\"not-signed-in\" data-sign-in=\"{url}\">This \
381 browser is not signed in as {account}. Sign it in at <a href=\"{url}\">{url}</a>.</p>",
382 url = escape(sign_in_url)
383 ),
384 };
385 let (status, tone) = match (pending.waiting(), pending.held) {
386 (true, _) => ("waiting for the account", Tone::Waiting),
387 (false, true) => ("approved: waiting for its browser", Tone::Waiting),
388 (false, false) => ("approved", Tone::Done),
389 };
390 let body = format!(
391 "<p data-state=\"{state}\">State: {state}</p>\
392 <p data-client-origin=\"{origin}\" data-client-key=\"{key}\" \
393 data-client-first-time=\"{first}\">{origin}{unverified}</p>\
394 <p data-account=\"{account}\">{account}</p>\
395 {browser}\
396 <div data-requested=\"{requested}\">This application has requested these scopes:\
397 {requested_items}</div>\
398 <div data-verdict=\"{verdict}\" data-granted=\"{granted}\" data-cut=\"{cut}\" \
399 data-rule=\"{rule}\" data-approves=\"granted\" \
400 data-ceiling-checked=\"each-use\">\
401 {allowed}</div>\
402 <p class=\"hint\" data-authorize-url=\"{url}\">Authorize URL: {url}</p>",
403 state = record.state.as_str(),
404 url = escape(authorize_url),
405 origin = escape(&record.client_origin),
406 key = escape(&record.client_key.0),
407 first = record.first_time,
408 unverified = if didbot_pds::policy::is_local_origin(&record.client_origin) {
409 ": an unverified local app"
410 } else {
411 ""
412 },
413 requested = escape(&record.requested.to_string()),
414 requested_items = items(&record.requested),
415 verdict = record.verdict.kind(),
416 granted = escape(&pending.granted.to_string()),
417 cut = escape(&pending.cut.to_string()),
418 rule = escape(&pending.rule),
419 );
420 landing::page(
421 StatusCode::OK,
422 &Landing {
423 placeholder: landing::AUTHORIZE,
424 tone,
425 status,
426 body: &body,
427 reload: pending
428 .reloads()
429 .then_some(crate::oauth::authorize::RELOAD_INTERVAL.as_secs()),
430 },
431 )
432}
433
434pub(crate) async fn oauth_authorize(
458 State(state): State<AppState>,
459 peer: Peer,
460 query: Result<Query<OAuthAuthorizeQuery>, QueryRejection>,
461 headers: HeaderMap,
462) -> Response {
463 landing_headers(authorize_answer(&state, peer, query, &headers))
464}
465
466fn landing_headers(mut response: Response) -> Response {
469 let answered = response.headers_mut();
470 answered.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
471 answered.insert(
472 header::CONTENT_SECURITY_POLICY,
473 HeaderValue::from_static(didbot_brand::page::content_security_policy()),
474 );
475 response
476}
477
478fn authorize_answer(
481 state: &AppState,
482 peer: Peer,
483 query: Result<Query<OAuthAuthorizeQuery>, QueryRejection>,
484 headers: &HeaderMap,
485) -> Response {
486 use crate::oauth::authorize::{answer, AuthorizeAnswer, Loader};
487 use crate::oauth::error::OAuthErrorCode;
488
489 let Query(query) = match query {
490 Ok(query) => query,
491 Err(rejection) => {
492 let refused = ApiError::bad_request(rejection.body_text());
493 crate::refusal::refused(crate::refusal::Refusal {
494 endpoint: "authorize",
495 client_id: None,
496 account: None,
497 error: refused.error(),
498 reason: refused.message(),
499 severity: Severity::Info,
500 });
501 return landing::refused(
502 StatusCode::BAD_REQUEST,
503 landing::AUTHORIZE,
504 "invalid_request",
505 "this sign-in link is incomplete",
506 "",
507 "Start the sign-in again from the app.",
508 );
509 }
510 };
511 let refuse = |refused: Refused, page: Response| {
513 refused.write("authorize", Some(&query.client_id), None);
514 page
515 };
516
517 let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
518 if !state.oauth.authorize_rate_limiter.check(&key) {
519 return refuse(
520 Refused::info(too_many_oauth_requests("authorize")),
521 landing::too_many(landing::AUTHORIZE),
522 );
523 }
524
525 let browser = crate::oauth::browser::presented(headers)
526 .and_then(|cookie| state.oauth.browser_sessions.browser(&cookie));
527 let loader = Loader {
528 accounts: browser
529 .as_ref()
530 .map(|browser| browser.accounts.clone())
531 .unwrap_or_default(),
532 navigation: headers
533 .get("sec-fetch-dest")
534 .is_none_or(|dest| dest == "document"),
535 };
536 confirm_remembered(state, &query.request_uri, &query.client_id, &loader);
537 let answered = match answer(
538 state.oauth.par_store.as_ref(),
539 state.oauth.decision_store.as_ref(),
540 &query.request_uri,
541 &query.client_id,
542 &loader,
543 time::OffsetDateTime::now_utc(),
544 ) {
545 Ok(answered) => answered,
546 Err(err) => {
547 return refuse(
548 Refused::info(authorize_error(err.clone())),
549 authorize_error_page(err),
550 )
551 }
552 };
553 let issuer = crate::oauth::discovery::issuer_for(state.registry.as_ref());
556 let response = match (answered, issuer) {
557 (AuthorizeAnswer::Redirect { to, account }, _) => {
558 info!(
559 account,
560 "a browser signed in as the account collected its code"
561 );
562 see_other(&to, landing::AUTHORIZE)
563 }
564 (AuthorizeAnswer::Denied(denial), Ok(issuer)) => match denial.redirect(&issuer) {
565 Ok(to) => see_other(to.as_str(), landing::AUTHORIZE),
566 Err(err) => {
567 let why = format!("the client's redirect is unusable: {err}");
568 refuse(
569 Refused::warn(OAuthError::new(OAuthErrorCode::ServerError, why.clone())),
570 landing::server_error(landing::AUTHORIZE, &why),
571 )
572 }
573 },
574 (AuthorizeAnswer::Page(pending), Ok(issuer)) => {
575 if pending.held {
576 info!(
577 account = pending.record.account,
578 signed_in = pending.signed_in,
579 "an approved sign-in's code waits for a browser signed in as the account"
580 );
581 }
582 let query = url::form_urlencoded::Serializer::new(String::new())
583 .append_pair("client_id", &query.client_id)
584 .append_pair("request_uri", &query.request_uri)
585 .finish();
586 let authorize_url = format!("{issuer}/oauth/authorize?{query}");
587 let sign_in_url = format!("{issuer}{}", crate::oauth::browser::PATH);
588 authorize_page(&pending, &authorize_url, &sign_in_url)
589 }
590 (_, Err(err)) => landing::server_error(landing::AUTHORIZE, &err.to_string()),
591 };
592 match browser.filter(|browser| browser.replaced) {
595 Some(browser) => with_browser_cookie(response, &browser, landing::AUTHORIZE),
596 None => response,
597 }
598}
599
600#[derive(serde::Deserialize)]
602pub(crate) struct OAuthBrowserQuery {
603 ticket: Option<String>,
605}
606
607pub(crate) async fn oauth_browser(
624 State(state): State<AppState>,
625 uri: axum::http::Uri,
626 query: Result<Query<OAuthBrowserQuery>, QueryRejection>,
627 headers: HeaderMap,
628) -> Response {
629 landing_headers(browser_answer(&state, &uri, query, &headers))
630}
631
632fn browser_answer(
635 state: &AppState,
636 uri: &axum::http::Uri,
637 query: Result<Query<OAuthBrowserQuery>, QueryRejection>,
638 headers: &HeaderMap,
639) -> Response {
640 use crate::oauth::browser::PATH;
641
642 const NEW_LINK: &str = "Ask the account for a new sign-in link.";
643 let Query(query) = match query {
644 Ok(query) => query,
645 Err(_) => {
646 return landing::refused(
647 StatusCode::BAD_REQUEST,
648 landing::BROWSER,
649 "InvalidRequest",
650 "this sign-in link is malformed",
651 "",
652 NEW_LINK,
653 )
654 }
655 };
656 let issuer = match crate::oauth::discovery::issuer_for(state.registry.as_ref()) {
657 Ok(issuer) => issuer,
658 Err(err) => return landing::server_error(landing::BROWSER, &err.to_string()),
659 };
660 if !on_issuer_host(&issuer, uri, headers) {
661 let here = escape(&format!("{issuer}{PATH}"));
662 return landing::refused(
663 StatusCode::FORBIDDEN,
664 landing::BROWSER,
665 "WrongHost",
666 "this is not this server's sign-in address",
667 &format!(
668 "<p data-refused=\"host\">This page is served only at \
669 <a href=\"{here}\">{here}</a>.</p>"
670 ),
671 "Open that address instead.",
672 );
673 }
674
675 let sessions = &state.oauth.browser_sessions;
676 let presented = crate::oauth::browser::presented(headers);
677 let Some(ticket) = query.ticket else {
678 return browser_list(presented.and_then(|cookie| sessions.browser(&cookie)));
679 };
680 if !followed_from_here(headers) {
681 info!("refused a browser sign-in link followed from another site");
682 return landing::refused(
683 StatusCode::FORBIDDEN,
684 landing::BROWSER,
685 "CrossSite",
686 "a sign-in link followed from another site is refused",
687 "<p data-refused=\"cross-site\">The link still works, once.</p>",
688 "Paste it into this browser's address bar.",
689 );
690 }
691 if let Err(halted) = state.estop.check_issue(EstopRefusal::Token) {
692 info!(%halted, "refused a browser sign-in while the e-stop is engaged");
693 return landing::refused(
694 StatusCode::SERVICE_UNAVAILABLE,
695 landing::BROWSER,
696 "Halted",
697 "this server has stopped signing anything in",
698 "",
699 "Try again once this server's operator releases the stop.",
700 );
701 }
702 let Some(signed_in) = sessions.redeem(&ticket, presented.as_deref()) else {
703 return landing::refused(
704 StatusCode::BAD_REQUEST,
705 landing::BROWSER,
706 "TicketNotLive",
707 "this sign-in link is not live",
708 "<p data-ticket=\"not-live\">It is unknown, used or expired.</p>",
709 NEW_LINK,
710 );
711 };
712 info!(account = signed_in.account, "a browser signed in");
713 with_browser_cookie(
714 see_other(PATH, landing::BROWSER),
715 &signed_in.browser,
716 landing::BROWSER,
717 )
718}
719
720fn on_issuer_host(issuer: &str, uri: &axum::http::Uri, headers: &HeaderMap) -> bool {
723 let Some(authority) = request_authority(uri, headers) else {
724 return false;
725 };
726 let host = host_without_port(authority);
727 url::Url::parse(issuer)
728 .ok()
729 .and_then(|issuer| issuer.host_str().map(str::to_owned))
730 .is_some_and(|expected| expected.eq_ignore_ascii_case(host))
731}
732
733fn followed_from_here(headers: &HeaderMap) -> bool {
736 headers
737 .get("sec-fetch-site")
738 .is_none_or(|site| matches!(site.to_str(), Ok("none" | "same-origin")))
739}
740
741fn browser_list(browser: Option<crate::oauth::browser::Browser>) -> Response {
744 let accounts = browser
745 .as_ref()
746 .map(|browser| browser.accounts.as_slice())
747 .unwrap_or_default();
748 let (status, tone, body) = if accounts.is_empty() {
749 (
750 "not signed in",
751 Tone::Waiting,
752 "<p data-accounts=\"0\">This browser is signed in as no account here.</p>\
753 <p class=\"next\">Open a sign-in link from an account to sign it in.</p>"
754 .to_owned(),
755 )
756 } else {
757 let items: String = accounts
758 .iter()
759 .map(|account| {
760 let account = escape(account);
761 format!("<li data-account=\"{account}\">{account}</li>")
762 })
763 .collect();
764 (
765 "signed in",
766 Tone::Done,
767 format!(
768 "<p data-accounts=\"{}\">This browser is signed in as:</p><ul>{items}</ul>\
769 <p class=\"next\">Go back to the sign-in that sent you here.</p>",
770 accounts.len()
771 ),
772 )
773 };
774 let page = landing::page(
775 StatusCode::OK,
776 &Landing {
777 placeholder: landing::BROWSER,
778 tone,
779 status,
780 body: &body,
781 reload: None,
782 },
783 );
784 match browser.filter(|browser| browser.replaced) {
785 Some(browser) => with_browser_cookie(page, &browser, landing::BROWSER),
786 None => page,
787 }
788}
789
790fn with_browser_cookie(
793 mut response: Response,
794 browser: &crate::oauth::browser::Browser,
795 placeholder: &str,
796) -> Response {
797 match HeaderValue::try_from(crate::oauth::browser::cookie(browser)) {
798 Ok(cookie) => {
799 response.headers_mut().append(header::SET_COOKIE, cookie);
800 response
801 }
802 Err(_) => landing::server_error(placeholder, "a browser session id is not a header value"),
803 }
804}
805
806fn see_other(location: &str, placeholder: &str) -> Response {
810 match HeaderValue::from_str(location) {
811 Ok(location) => (StatusCode::SEE_OTHER, [(header::LOCATION, location)]).into_response(),
812 Err(_) => landing::server_error(placeholder, "the client's redirect is not a header value"),
813 }
814}
815
816#[derive(serde::Deserialize)]
819pub(crate) struct OAuthTokenForm {
820 grant_type: String,
821 code: Option<String>,
822 redirect_uri: Option<String>,
823 code_verifier: Option<String>,
824 client_id: Option<String>,
825 refresh_token: Option<String>,
826 client_assertion_type: Option<String>,
827 client_assertion: Option<String>,
828}
829
830pub(crate) async fn never_cached(
838 request: axum::extract::Request,
839 next: axum::middleware::Next,
840) -> Response {
841 let mut response = next.run(request).await;
842 let headers = response.headers_mut();
843 headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
844 headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
845 response
846}
847
848fn checked_proof(
857 state: &AppState,
858 headers: &HeaderMap,
859 path: &str,
860) -> Result<crate::oauth::dpop_seam::VerifiedProof, crate::oauth::error::OAuthError> {
861 use crate::oauth::dpop_seam::DpopError;
862 use crate::oauth::error::{OAuthError, OAuthErrorCode};
863
864 let proof = match crate::oauth::dpop::presented_proof(headers) {
865 Ok(Some(proof)) => proof,
866 Ok(None) => {
867 return Err(OAuthError::new(
868 OAuthErrorCode::InvalidDpopProof,
869 "this request carries no DPoP proof",
870 ))
871 }
872 Err(err) => {
873 return Err(OAuthError::new(
874 OAuthErrorCode::InvalidDpopProof,
875 err.to_string(),
876 ))
877 }
878 };
879 let htu = crate::oauth::discovery::request_htu(state.registry.as_ref(), path)
883 .map_err(|err| OAuthError::new(OAuthErrorCode::ServerError, err.to_string()))?;
884 state
887 .oauth
888 .dpop
889 .verify(proof, "POST", &htu, None)
890 .map_err(|err| match err {
891 DpopError::UseNonce { nonce, message } => {
892 OAuthError::new(OAuthErrorCode::UseDpopNonce, message).with_dpop_nonce(nonce)
893 }
894 err => OAuthError::new(OAuthErrorCode::InvalidDpopProof, err.to_string()),
895 })
896}
897
898pub(crate) fn oauth_token_response(tokens: crate::oauth::token::OAuthTokens) -> Response {
899 let expires_in = (tokens.access_expires_at - time::OffsetDateTime::now_utc())
900 .whole_seconds()
901 .max(0);
902 Json(json!({
903 "token_type": "DPoP",
907 "access_token": tokens.access_token,
908 "expires_in": expires_in,
909 "refresh_token": tokens.refresh_token,
910 "scope": tokens.scope.to_string(),
911 "sub": tokens.did,
912 }))
913 .into_response()
914}
915
916pub(crate) fn token_refusal(err: crate::oauth::token::TokenError) -> Refused {
925 use crate::oauth::error::OAuthErrorCode;
926 use crate::oauth::token::TokenError;
927 let code = match err {
928 TokenError::UnsupportedGrantType(_) => OAuthErrorCode::UnsupportedGrantType,
929 TokenError::MissingParameter(_) => OAuthErrorCode::InvalidRequest,
930 TokenError::InvalidCode
931 | TokenError::InvalidRefreshToken
932 | TokenError::RefreshReused
933 | TokenError::DpopKeyMismatch
934 | TokenError::RedirectUriMismatch
935 | TokenError::PkceFailed
936 | TokenError::ClientMismatch
937 | TokenError::ClientAuthenticationChanged
938 | TokenError::ClientRefused => OAuthErrorCode::InvalidGrant,
939 TokenError::ClientAssertionRequired => OAuthErrorCode::InvalidClient,
940 TokenError::Dpop(_) | TokenError::CodeKeyMismatch => OAuthErrorCode::InvalidDpopProof,
941 TokenError::NotKept => OAuthErrorCode::ServerError,
942 };
943 let error = OAuthError::new(code, err.to_string());
944 match err {
945 TokenError::RefreshReused
946 | TokenError::DpopKeyMismatch
947 | TokenError::CodeKeyMismatch
948 | TokenError::ClientAuthenticationChanged
949 | TokenError::ClientAssertionRequired
950 | TokenError::NotKept => Refused::warn(error),
951 _ => Refused::info(error),
952 }
953}
954
955pub const MAX_AUTHORIZATION_WAIT: u64 = 30;
962
963#[derive(serde::Deserialize)]
965pub(crate) struct ListPendingAuthorizationsQuery {
966 cursor: Option<String>,
971 wait: Option<u64>,
974}
975
976#[derive(serde::Deserialize)]
978#[serde(rename_all = "camelCase")]
979pub(crate) struct GetAuthorizationQuery {
980 request_uri: String,
982}
983
984pub(crate) async fn list_pending_authorizations(
1000 State(state): State<AppState>,
1001 headers: HeaderMap,
1002 query: Result<Query<ListPendingAuthorizationsQuery>, QueryRejection>,
1003) -> Response {
1004 let Query(query) = match query {
1005 Ok(query) => query,
1006 Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
1007 };
1008 let did = match auth::require_agent_token(state.registry.as_ref(), &headers) {
1009 Ok(did) => did,
1010 Err(err) => return err.into_response(),
1011 };
1012 let cursor = query.cursor.as_deref();
1013 let wait = std::time::Duration::from_secs(query.wait.unwrap_or(0).min(MAX_AUTHORIZATION_WAIT));
1014
1015 let deadline = tokio::time::Instant::now() + wait;
1016 let arrivals = state.oauth.decision_store.arrivals();
1017 let shutdown = state.oauth.shutdown.clone();
1018 loop {
1019 let notified = arrivals.notified();
1025 let stopping = shutdown.clone().wait();
1026 let (pending, next) = state.oauth.decision_store.pending_for(&did, cursor);
1027 if !pending.is_empty() || tokio::time::Instant::now() >= deadline {
1028 return pending_authorizations(&pending, &next);
1029 }
1030 let waited = tokio::select! {
1031 () = notified => true,
1032 () = stopping => false,
1033 () = tokio::time::sleep_until(deadline) => false,
1034 };
1035 if !waited {
1036 let (pending, next) = state.oauth.decision_store.pending_for(&did, cursor);
1037 return pending_authorizations(&pending, &next);
1038 }
1039 }
1040}
1041
1042pub(crate) fn pending_authorizations(
1045 pending: &[crate::oauth::decision::DecisionRecord],
1046 next: &str,
1047) -> Response {
1048 let pending: Vec<serde_json::Value> = pending
1049 .iter()
1050 .map(crate::oauth::decision::DecisionRecord::to_json)
1051 .collect();
1052 Json(json!({ "cursor": next, "pending": pending })).into_response()
1053}
1054
1055pub(crate) async fn get_authorization(
1062 State(state): State<AppState>,
1063 headers: HeaderMap,
1064 query: Result<Query<GetAuthorizationQuery>, QueryRejection>,
1065) -> Response {
1066 let Query(query) = match query {
1067 Ok(query) => query,
1068 Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
1069 };
1070 let did = match auth::require_agent_token(state.registry.as_ref(), &headers) {
1071 Ok(did) => did,
1072 Err(err) => return err.into_response(),
1073 };
1074 match state.oauth.decision_store.get(&query.request_uri) {
1075 Some(record) if record.account == did => Json(record.to_json()).into_response(),
1076 _ => ApiError::new(
1077 StatusCode::NOT_FOUND,
1078 "AuthorizationNotFound",
1079 "no authorization request of yours answers to that request_uri",
1080 )
1081 .into_response(),
1082 }
1083}
1084
1085pub(crate) fn decision_by_token(
1094 state: &AppState,
1095 did: &str,
1096 token: &str,
1097) -> Result<crate::oauth::decision::DecisionRecord, ApiError> {
1098 let unknown =
1099 || ApiError::bad_request("this approval token is unknown, already used, or expired");
1100 let record = state
1101 .oauth
1102 .decision_store
1103 .by_token(token)
1104 .ok_or_else(unknown)?;
1105 if record.account != did {
1106 return Err(unknown());
1107 }
1108 if !record.is_live(time::OffsetDateTime::now_utc()) {
1109 return Err(unknown());
1110 }
1111 Ok(record)
1112}
1113
1114pub(crate) async fn approve_authorization(
1143 State(state): State<AppState>,
1144 headers: HeaderMap,
1145 XrpcBody(body): XrpcBody,
1146) -> Response {
1147 if let Err(halted) = state.estop.check_issue(EstopRefusal::Token) {
1148 return ApiError::from(halted).into_response();
1149 }
1150 let request: crate::wire::ApproveAuthorizationRequest = match parse_body(&body) {
1151 Ok(request) => request,
1152 Err(err) => return err.into_response(),
1153 };
1154 let did = match auth::require_agent_token(state.registry.as_ref(), &headers) {
1155 Ok(did) => did,
1156 Err(err) => return err.into_response(),
1157 };
1158 let record = match decision_by_token(&state, &did, &request.token) {
1159 Ok(record) => record,
1160 Err(err) => return err.into_response(),
1161 };
1162 if let crate::oauth::decision::Verdict::Deny { reason, rule } = &record.verdict {
1163 info!(account = did, rule, "approval refused: the decision denies");
1164 return ApiError::bad_request(format!(
1165 "this authorization request was refused by `{rule}`: {reason}"
1166 ))
1167 .into_response();
1168 }
1169 let issued = match issue_approval(&state, &did, &record, request.token) {
1170 Ok(issued) => issued,
1171 Err(err) => return err.into_response(),
1172 };
1173 state.oauth.decision_store.remember(
1174 &did,
1175 &record.client_key,
1176 &issued.client_id,
1177 issued.granted.clone(),
1178 time::OffsetDateTime::now_utc(),
1179 );
1180 if issued.browser {
1181 info!(
1182 account = did,
1183 "approved; a browser signed in as the account collects the code"
1184 );
1185 } else {
1186 info!(account = did, "approved; the caller delivers the code");
1187 }
1188
1189 Json(json!({
1190 "requestUri": record.request_uri,
1191 "granted": issued.granted.0.iter().map(ToString::to_string).collect::<Vec<_>>(),
1192 "redirect": issued.redirect.as_str(),
1193 }))
1194 .into_response()
1195}
1196
1197struct Issued {
1199 client_id: String,
1201 granted: crate::oauth::scope::ScopeSet,
1203 redirect: url::Url,
1205 browser: bool,
1208}
1209
1210fn issue_approval(
1218 state: &AppState,
1219 did: &str,
1220 record: &crate::oauth::decision::DecisionRecord,
1221 token: String,
1222) -> Result<Issued, ApiError> {
1223 let issuer = crate::oauth::discovery::issuer_for(state.registry.as_ref()).map_err(|err| {
1226 ApiError::new(
1227 StatusCode::INTERNAL_SERVER_ERROR,
1228 "ServerError",
1229 err.to_string(),
1230 )
1231 })?;
1232
1233 let Some(pushed) = state.oauth.par_store.get(&record.request_uri) else {
1234 return Err(ApiError::bad_request(
1235 "this authorization request is not live",
1236 ));
1237 };
1238
1239 let reference = crate::oauth::consent::ConsentReference(token);
1242 let pending =
1243 crate::oauth::consent::confirm(state.oauth.consent_store.as_ref(), &reference, did)
1244 .map_err(|err| {
1245 info!(account = did, error = %err, "consent refused");
1246 ApiError::bad_request(err.to_string())
1247 })?;
1248
1249 let granted = record.granted();
1250 let redirect_uri = pending.redirect_uri.clone();
1251 let state_param = pending.state.clone();
1252 let code = state.oauth.code_store.issue(pending.into());
1253 let redirect = match crate::oauth::authorize::authorization_response(
1256 &redirect_uri,
1257 pushed.response_mode,
1258 &[("code", &code)],
1259 state_param.as_deref(),
1260 &issuer,
1261 ) {
1262 Ok(redirect) => redirect,
1263 Err(err) => {
1264 state.oauth.code_store.redeem(&code);
1265 return Err(ApiError::bad_request(format!(
1266 "the client's redirect is unusable: {err}"
1267 )));
1268 }
1269 };
1270 let browser =
1271 crate::oauth::authorize::browser_carries(&pushed.redirect_uri, pushed.response_mode);
1272 if state
1276 .oauth
1277 .decision_store
1278 .approve(&record.request_uri, browser.then(|| redirect.to_string()))
1279 .is_none()
1280 {
1281 state.oauth.code_store.redeem(&code);
1282 return Err(ApiError::bad_request(
1283 "this authorization request has already been answered",
1284 ));
1285 }
1286 Ok(Issued {
1287 client_id: pushed.client_id,
1288 granted,
1289 redirect,
1290 browser,
1291 })
1292}
1293
1294fn confirm_remembered(
1302 state: &AppState,
1303 request_uri: &str,
1304 client_id: &str,
1305 loader: &crate::oauth::authorize::Loader,
1306) {
1307 let now = time::OffsetDateTime::now_utc();
1308 let (Some(pushed), Some(record)) = (
1309 state.oauth.par_store.get(request_uri),
1310 state.oauth.decision_store.get(request_uri),
1311 ) else {
1312 return;
1313 };
1314 if pushed.client_id != client_id {
1315 return;
1316 }
1317 let remembered =
1318 state
1319 .oauth
1320 .decision_store
1321 .remembered(&record.account, &record.client_key, now);
1322 if !crate::oauth::authorize::confirms_unasked(
1323 &pushed,
1324 &record,
1325 remembered.as_ref(),
1326 loader,
1327 now,
1328 ) {
1329 return;
1330 }
1331 if state.estop.check_issue(EstopRefusal::Token).is_err() {
1334 return;
1335 }
1336 let Some(token) = record.token.clone() else {
1337 return;
1338 };
1339 match issue_approval(state, &record.account, &record, token.0) {
1340 Ok(_) => info!(
1341 account = record.account,
1342 client = pushed.client_id,
1343 "confirmed a sign-in on the account's remembered approval"
1344 ),
1345 Err(err) => info!(
1346 account = record.account,
1347 client = pushed.client_id,
1348 error = err.message(),
1349 "a remembered approval did not confirm a sign-in"
1350 ),
1351 }
1352}
1353
1354pub(crate) async fn create_browser_sign_in(
1365 State(state): State<AppState>,
1366 headers: HeaderMap,
1367) -> Response {
1368 if let Err(halted) = state.estop.check_issue(EstopRefusal::Token) {
1369 return ApiError::from(halted).into_response();
1370 }
1371 let did = match auth::require_agent_token(state.registry.as_ref(), &headers) {
1372 Ok(did) => did,
1373 Err(err) => return err.into_response(),
1374 };
1375 let issuer = match crate::oauth::discovery::issuer_for(state.registry.as_ref()) {
1376 Ok(issuer) => issuer,
1377 Err(err) => {
1378 return ApiError::new(
1379 StatusCode::INTERNAL_SERVER_ERROR,
1380 "ServerError",
1381 err.to_string(),
1382 )
1383 .into_response()
1384 }
1385 };
1386 let minted = state.oauth.browser_sessions.mint(&did);
1387 info!(account = did, "minted a browser sign-in link");
1388 Json(json!({
1389 "url": format!("{issuer}{}?ticket={}", crate::oauth::browser::PATH, minted.ticket),
1390 "expiresAt": didbot_schema::format::datetime::render(minted.expires_at),
1391 }))
1392 .into_response()
1393}
1394
1395pub(crate) async fn decline_authorization(
1402 State(state): State<AppState>,
1403 headers: HeaderMap,
1404 XrpcBody(body): XrpcBody,
1405) -> Response {
1406 let request: crate::wire::DeclineAuthorizationRequest = match parse_body(&body) {
1407 Ok(request) => request,
1408 Err(err) => return err.into_response(),
1409 };
1410 let did = match auth::require_agent_token(state.registry.as_ref(), &headers) {
1411 Ok(did) => did,
1412 Err(err) => return err.into_response(),
1413 };
1414 let record = match decision_by_token(&state, &did, &request.token) {
1415 Ok(record) => record,
1416 Err(err) => return err.into_response(),
1417 };
1418 if state
1419 .oauth
1420 .decision_store
1421 .settle(
1422 &record.request_uri,
1423 crate::oauth::decision::DecisionState::Declined,
1424 )
1425 .is_none()
1426 {
1427 return ApiError::bad_request("this authorization request has already been answered")
1428 .into_response();
1429 }
1430 state
1432 .oauth
1433 .consent_store
1434 .consume(&crate::oauth::consent::ConsentReference(request.token));
1435 info!(
1436 account = did,
1437 reason = request.reason.as_deref().unwrap_or(""),
1438 "authorization declined"
1439 );
1440 Json(json!({ "requestUri": record.request_uri })).into_response()
1441}
1442
1443async fn authenticated_client(
1451 state: &AppState,
1452 client_id: Option<&str>,
1453 credentials: &crate::oauth::client_auth::ClientCredentials,
1454) -> Result<Option<crate::oauth::client_auth::AuthenticatedClient>, Refused> {
1455 use crate::oauth::error::OAuthErrorCode;
1456
1457 if credentials.is_empty() {
1458 return Ok(None);
1459 }
1460 let Some(client_id) = client_id else {
1461 return Err(Refused::info(OAuthError::new(
1462 OAuthErrorCode::InvalidRequest,
1463 "a client_assertion needs the client_id it authenticates",
1464 )));
1465 };
1466 if let Err(error) = didbot_pds::policy::ClientId::parse(client_id) {
1469 return Err(Refused::info(OAuthError::new(
1470 OAuthErrorCode::InvalidClient,
1471 error.to_string(),
1472 )));
1473 }
1474 let issuer = crate::oauth::discovery::issuer_for(state.registry.as_ref()).map_err(|err| {
1475 Refused::warn(OAuthError::new(
1476 OAuthErrorCode::ServerError,
1477 err.to_string(),
1478 ))
1479 })?;
1480 let (document, _) = crate::oauth::client_metadata::resolve_client(
1481 &state.oauth.http,
1482 state.oauth.client_cache.as_ref(),
1483 client_id,
1484 crate::oauth::client_metadata::LoopbackClients::for_zone(state.registry.zone()),
1485 )
1486 .await
1487 .map_err(|error| {
1488 Refused::warn(OAuthError::new(
1492 OAuthErrorCode::InvalidClient,
1493 crate::oauth::par::ParError::ClientMetadata(error.clone()).to_string(),
1494 ))
1495 .because(error)
1496 })?;
1497 let key = state
1498 .oauth
1499 .client_assertions
1500 .authenticate(&document, credentials, &issuer)
1501 .map_err(|error| Refused::warn(OAuthError::new(error.code(), error.to_string())))?;
1502 Ok(
1503 key.map(|key| crate::oauth::client_auth::AuthenticatedClient {
1504 client_id: client_id.to_owned(),
1505 key,
1506 }),
1507 )
1508}
1509
1510pub(crate) async fn oauth_token(
1524 State(state): State<AppState>,
1525 peer: Peer,
1526 headers: HeaderMap,
1527 axum::Form(form): axum::Form<OAuthTokenForm>,
1528) -> Response {
1529 use crate::oauth::error::OAuthErrorCode;
1530
1531 let client_id = form.client_id.clone();
1532 let refuse = |refused: Refused| refused.answer("token", client_id.as_deref(), None);
1533
1534 if let Err(halted) = state.estop.check_issue(EstopRefusal::Token) {
1535 let refused = ApiError::from(halted);
1536 crate::refusal::refused(crate::refusal::Refusal {
1537 endpoint: "token",
1538 client_id: client_id.as_deref(),
1539 account: None,
1540 error: refused.error(),
1541 reason: refused.message(),
1542 severity: Severity::Info,
1543 });
1544 return refused.into_response();
1545 }
1546
1547 let key = caller_key(&headers, peer.0, state.trust_forwarded_headers);
1551 if !state.oauth.token_rate_limiter.check(&key) {
1552 return refuse(Refused::info(too_many_oauth_requests("token")));
1553 }
1554
1555 let proof = match checked_proof(&state, &headers, "/oauth/token") {
1556 Ok(proof) => proof,
1557 Err(refused) => return refuse(Refused::info(refused)),
1558 };
1559
1560 let pds_did = deployment_did(&state);
1561 let pds_hostname = state.registry.zone().to_owned();
1562 let admission = crate::oauth::token::GateAdmission {
1563 gate: state.oauth.grant_gate.as_ref(),
1564 registry: state.registry.as_ref(),
1565 pds_did: &pds_did,
1566 pds_hostname: &pds_hostname,
1567 };
1568
1569 let credentials = crate::oauth::client_auth::ClientCredentials {
1570 assertion_type: form.client_assertion_type,
1571 assertion: form.client_assertion,
1572 };
1573 let client = match authenticated_client(&state, form.client_id.as_deref(), &credentials).await {
1574 Ok(client) => client,
1575 Err(refused) => return refuse(refused),
1576 };
1577
1578 match form.grant_type.as_str() {
1579 "authorization_code" => {
1580 let (Some(code), Some(redirect_uri), Some(code_verifier), Some(client_id)) = (
1581 form.code,
1582 form.redirect_uri,
1583 form.code_verifier,
1584 form.client_id,
1585 ) else {
1586 return refuse(Refused::info(OAuthError::new(
1587 OAuthErrorCode::InvalidRequest,
1588 "authorization_code grant needs code, redirect_uri, code_verifier, and client_id",
1589 )));
1590 };
1591 match state.oauth.tokens.exchange_code(
1592 state.oauth.code_store.as_ref(),
1593 state.oauth.dpop.as_ref(),
1594 &admission,
1595 state.oauth.scope_policy.as_ref(),
1596 &code,
1597 &redirect_uri,
1598 &code_verifier,
1599 &client_id,
1600 client.as_ref(),
1601 &proof,
1602 ) {
1603 Ok(tokens) => oauth_token_response(tokens),
1604 Err(err) => refuse(token_refusal(err)),
1605 }
1606 }
1607 "refresh_token" => {
1608 let Some(refresh_token) = form.refresh_token else {
1609 return refuse(Refused::info(OAuthError::new(
1610 OAuthErrorCode::InvalidRequest,
1611 "refresh_token grant needs refresh_token",
1612 )));
1613 };
1614 match state.oauth.tokens.refresh(
1615 state.oauth.dpop.as_ref(),
1616 &admission,
1617 state.oauth.scope_policy.as_ref(),
1618 &refresh_token,
1619 client.as_ref(),
1620 &proof,
1621 ) {
1622 Ok(tokens) => oauth_token_response(tokens),
1623 Err(err) => refuse(token_refusal(err)),
1624 }
1625 }
1626 other => refuse(Refused::info(OAuthError::new(
1627 OAuthErrorCode::UnsupportedGrantType,
1628 format!("grant_type `{other}` is not supported"),
1629 ))),
1630 }
1631}