1use std::sync::Arc;
33
34use serde::{Deserialize, Serialize};
35use tracing::debug;
36
37use solid_pod_rs::oidc::verify_dpop_proof;
38
39use crate::error::ProviderError;
40use crate::jwks::Jwks;
41use crate::registration::ClientStore;
42use crate::session::{AuthCodeRecord, SessionStore};
43use crate::tokens::{issue_access_token, AccessToken};
44use crate::user_store::UserStore;
45
46#[derive(Debug, Clone)]
48pub struct ProviderConfig {
49 pub issuer: String,
51 pub access_token_ttl_secs: u64,
53 pub dpop_skew_secs: u64,
55}
56
57impl ProviderConfig {
58 pub fn new(issuer: impl Into<String>) -> Self {
60 Self {
61 issuer: issuer.into(),
62 access_token_ttl_secs: 3600,
63 dpop_skew_secs: 60,
64 }
65 }
66}
67
68#[derive(Clone)]
70pub struct Provider {
71 config: ProviderConfig,
72 client_store: ClientStore,
73 session_store: SessionStore,
74 user_store: Arc<dyn UserStore>,
75 jwks: Jwks,
76}
77
78impl Provider {
79 pub fn new(
81 config: ProviderConfig,
82 client_store: ClientStore,
83 session_store: SessionStore,
84 user_store: Arc<dyn UserStore>,
85 jwks: Jwks,
86 ) -> Self {
87 Self {
88 config,
89 client_store,
90 session_store,
91 user_store,
92 jwks,
93 }
94 }
95
96 pub fn jwks(&self) -> &Jwks {
98 &self.jwks
99 }
100
101 pub fn config(&self) -> &ProviderConfig {
103 &self.config
104 }
105
106 pub fn client_store(&self) -> &ClientStore {
109 &self.client_store
110 }
111
112 pub fn session_store(&self) -> &SessionStore {
114 &self.session_store
115 }
116
117 pub fn user_store_trait_object(&self) -> &dyn UserStore {
121 self.user_store.as_ref()
122 }
123
124 pub fn discovery_document(&self) -> crate::discovery::DiscoveryDocument {
126 crate::discovery::build_discovery(&self.config.issuer)
127 }
128
129 pub async fn authorize(
137 &self,
138 req: AuthorizeRequest,
139 ) -> Result<AuthorizeResponse, ProviderError> {
140 let client = self
142 .client_store
143 .find(&req.client_id)
144 .await
145 .map_err(|e| ProviderError::ClientDocument(e.to_string()))?
146 .ok_or_else(|| ProviderError::InvalidClient(format!("unknown: {}", req.client_id)))?;
147
148 if req.response_type != "code" {
150 return Err(ProviderError::InvalidRequest(format!(
151 "response_type must be 'code', got '{}'",
152 req.response_type
153 )));
154 }
155
156 if !client.redirect_uris.iter().any(|r| r == &req.redirect_uri) {
158 return Err(ProviderError::InvalidRequest(format!(
159 "redirect_uri not registered: {}",
160 req.redirect_uri
161 )));
162 }
163
164 if req.code_challenge_method.as_deref() != Some("S256") {
168 return Err(ProviderError::InvalidRequest(
169 "PKCE S256 is required (code_challenge_method)".into(),
170 ));
171 }
172 if req.code_challenge.is_none() {
173 return Err(ProviderError::InvalidRequest(
174 "code_challenge is required".into(),
175 ));
176 }
177
178 match req.session_account_id {
180 None => Ok(AuthorizeResponse::NeedsLogin {
181 client_id: req.client_id,
182 redirect_uri: req.redirect_uri,
183 state: req.state,
184 code_challenge: req.code_challenge,
185 scope: req.scope,
186 }),
187 Some(account_id) => {
188 let code = self.session_store.issue_code(
189 &client.client_id,
190 account_id,
191 &req.redirect_uri,
192 req.code_challenge.clone(),
193 req.scope.clone(),
194 );
195 Ok(AuthorizeResponse::Redirect {
196 redirect_uri: req.redirect_uri,
197 code: code.code,
198 state: req.state,
199 iss: self.config.issuer.clone(),
200 })
201 }
202 }
203 }
204
205 pub async fn token(&self, req: TokenRequest<'_>) -> Result<TokenResponse, ProviderError> {
211 if req.grant_type != "authorization_code" {
212 return Err(ProviderError::InvalidRequest(format!(
213 "grant_type must be 'authorization_code', got '{}'",
214 req.grant_type
215 )));
216 }
217
218 let dpop_proof = req
221 .dpop_proof
222 .ok_or_else(|| ProviderError::InvalidDpop("missing DPoP header".into()))?;
223
224 let expected_htu = format!(
225 "{}/idp/token",
226 self.config.issuer.trim_end_matches('/')
227 );
228 let verified = verify_dpop_proof(
229 dpop_proof,
230 &expected_htu,
231 "POST",
232 req.now_unix,
233 self.config.dpop_skew_secs,
234 None, )
236 .await
237 .map_err(|e| ProviderError::InvalidDpop(e.to_string()))?;
238 let jkt = verified.jkt;
239
240 let code: AuthCodeRecord = self
242 .session_store
243 .take_code(req.code)
244 .ok_or_else(|| ProviderError::InvalidGrant("code expired or unknown".into()))?;
245
246 if code.client_id != req.client_id {
248 return Err(ProviderError::InvalidGrant(
249 "code issued to different client_id".into(),
250 ));
251 }
252 if code.redirect_uri != req.redirect_uri {
253 return Err(ProviderError::InvalidGrant(
254 "redirect_uri mismatch".into(),
255 ));
256 }
257
258 if let Some(challenge) = &code.code_challenge {
261 let verifier = req.code_verifier.ok_or_else(|| {
262 ProviderError::InvalidRequest("code_verifier required for PKCE".into())
263 })?;
264 let computed = pkce_s256(verifier);
265 if &computed != challenge {
266 return Err(ProviderError::InvalidGrant(
267 "PKCE verifier mismatch".into(),
268 ));
269 }
270 }
271
272 let user = self
274 .user_store
275 .find_by_id(&code.account_id)
276 .await
277 .map_err(|e| ProviderError::UserStore(e.to_string()))?
278 .ok_or_else(|| ProviderError::InvalidGrant("account not found".into()))?;
279
280 let key = self.jwks.active_key();
282 let token: AccessToken = issue_access_token(
283 &key,
284 &self.config.issuer,
285 &user.webid,
286 &user.id,
287 &code.client_id,
288 code.requested_scope.as_deref().unwrap_or("openid webid"),
289 Some(&jkt),
290 req.now_unix,
291 self.config.access_token_ttl_secs,
292 )
293 .map_err(|e| ProviderError::Crypto(e.to_string()))?;
294
295 debug!(client_id = %code.client_id, webid = %user.webid, "issued DPoP-bound access token");
296
297 Ok(TokenResponse {
298 access_token: token.jwt,
299 token_type: "DPoP".into(),
300 expires_in: self.config.access_token_ttl_secs,
301 scope: token.payload.scope,
302 webid: Some(user.webid),
303 })
304 }
305
306 pub async fn userinfo(
310 &self,
311 access_token: &str,
312 dpop_jkt: &str,
313 now_unix: u64,
314 ) -> Result<UserInfo, ProviderError> {
315 let keyset = build_jwk_set(&self.jwks);
321 let v = solid_pod_rs::oidc::verify_access_token(
322 access_token,
323 &solid_pod_rs::oidc::TokenVerifyKey::Asymmetric(keyset),
324 &self.config.issuer,
325 dpop_jkt,
326 now_unix,
327 )
328 .map_err(|e| ProviderError::InvalidDpop(e.to_string()))?;
329
330 Ok(UserInfo {
331 webid: v.webid.clone(),
332 sub: v.webid,
333 client_id: v.client_id,
334 scope: v.scope,
335 })
336 }
337}
338
339fn build_jwk_set(jwks: &Jwks) -> jsonwebtoken::jwk::JwkSet {
342 let doc = jwks.public_document();
343 let keys: Vec<jsonwebtoken::jwk::Jwk> = doc
344 .keys
345 .iter()
346 .filter_map(|k| serde_json::to_value(k).ok())
347 .filter_map(|v| serde_json::from_value(v).ok())
348 .collect();
349 jsonwebtoken::jwk::JwkSet { keys }
350}
351
352fn pkce_s256(verifier: &str) -> String {
354 use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
355 use base64::Engine;
356 use sha2::{Digest, Sha256};
357 let hash = Sha256::digest(verifier.as_bytes());
358 B64.encode(hash)
359}
360
361#[derive(Debug, Clone)]
363pub struct AuthorizeRequest {
364 pub client_id: String,
366 pub response_type: String,
368 pub redirect_uri: String,
371 pub state: Option<String>,
373 pub code_challenge: Option<String>,
375 pub code_challenge_method: Option<String>,
377 pub scope: Option<String>,
379 pub session_account_id: Option<String>,
383}
384
385#[derive(Debug, Clone)]
387pub enum AuthorizeResponse {
388 Redirect {
391 redirect_uri: String,
393 code: String,
395 state: Option<String>,
397 iss: String,
399 },
400 NeedsLogin {
402 client_id: String,
406 redirect_uri: String,
408 state: Option<String>,
410 code_challenge: Option<String>,
412 scope: Option<String>,
414 },
415}
416
417#[derive(Debug, Clone)]
419pub struct TokenRequest<'a> {
420 pub grant_type: String,
422 pub code: &'a str,
424 pub redirect_uri: String,
426 pub client_id: String,
428 pub code_verifier: Option<&'a str>,
430 pub dpop_proof: Option<&'a str>,
432 pub now_unix: u64,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct TokenResponse {
440 pub access_token: String,
442 pub token_type: String,
444 pub expires_in: u64,
446 pub scope: String,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub webid: Option<String>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct UserInfo {
457 pub sub: String,
459 pub webid: String,
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub client_id: Option<String>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub scope: Option<String>,
468}
469
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
475 use base64::Engine;
476 use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
477 use rand::Rng;
478 use serde_json::json;
479
480 use crate::registration::{register_client, ClientDocument, RegistrationRequest};
481 use crate::user_store::InMemoryUserStore;
482
483 async fn seed_provider() -> (Provider, InMemoryUserStore, ClientDocument, String) {
484 let store = Arc::new(InMemoryUserStore::new());
485 store
486 .insert_user(
487 "acct-1",
488 "alice@example.com",
489 "https://alice.example/profile#me",
490 None,
491 "hunter2!",
492 )
493 .unwrap();
494 let jwks = Jwks::generate_es256().unwrap();
495 let clients = ClientStore::new();
496 let client = register_client(
497 &clients,
498 RegistrationRequest {
499 redirect_uris: vec!["https://app.example/cb".into()],
500 client_name: Some("TestApp".into()),
501 ..Default::default()
502 },
503 )
504 .await
505 .unwrap();
506 let sessions = SessionStore::new();
507 let cfg = ProviderConfig::new("https://pod.example/");
508 let provider = Provider::new(cfg, clients, sessions, store.clone() as Arc<dyn UserStore>, jwks);
509
510 let verifier: String = (0..43)
511 .map(|_| {
512 let c = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
513 c[rand::thread_rng().gen_range(0..c.len())] as char
514 })
515 .collect();
516
517 (provider, InMemoryUserStore::new(), client, verifier)
518 }
519
520 fn s256(s: &str) -> String {
521 use sha2::{Digest, Sha256};
522 B64.encode(Sha256::digest(s.as_bytes()))
523 }
524
525 fn test_dpop_proof(htu: &str, htm: &str, iat: u64) -> String {
529 let mut sec = [0u8; 32];
531 rand::thread_rng().fill(&mut sec);
532 let k = B64.encode(sec);
533 let jwk = json!({
534 "kty": "oct",
535 "k": k,
536 });
537 let mut header = Header::new(Algorithm::HS256);
538 header.typ = Some("dpop+jwt".into());
539 header.jwk = Some(serde_json::from_value(jwk).unwrap());
540 let claims = json!({
541 "htu": htu,
542 "htm": htm,
543 "iat": iat,
544 "jti": uuid::Uuid::new_v4().to_string(),
545 });
546 encode(&header, &claims, &EncodingKey::from_secret(&sec)).unwrap()
547 }
548
549 #[tokio::test]
550 async fn authorize_needs_login_without_session() {
551 let (p, _, client, _) = seed_provider().await;
552 let req = AuthorizeRequest {
553 client_id: client.client_id.clone(),
554 response_type: "code".into(),
555 redirect_uri: "https://app.example/cb".into(),
556 state: Some("xyz".into()),
557 code_challenge: Some(s256("verifier-1")),
558 code_challenge_method: Some("S256".into()),
559 scope: Some("openid webid".into()),
560 session_account_id: None,
561 };
562 match p.authorize(req).await.unwrap() {
563 AuthorizeResponse::NeedsLogin { client_id, .. } => {
564 assert_eq!(client_id, client.client_id);
565 }
566 other => panic!("expected NeedsLogin, got {other:?}"),
567 }
568 }
569
570 #[tokio::test]
571 async fn authorize_issues_code_when_logged_in() {
572 let (p, _, client, verifier) = seed_provider().await;
573 let challenge = s256(&verifier);
574 let req = AuthorizeRequest {
575 client_id: client.client_id.clone(),
576 response_type: "code".into(),
577 redirect_uri: "https://app.example/cb".into(),
578 state: Some("state-1".into()),
579 code_challenge: Some(challenge),
580 code_challenge_method: Some("S256".into()),
581 scope: Some("openid webid".into()),
582 session_account_id: Some("acct-1".into()),
583 };
584 match p.authorize(req).await.unwrap() {
585 AuthorizeResponse::Redirect {
586 redirect_uri,
587 code,
588 state,
589 iss,
590 } => {
591 assert_eq!(redirect_uri, "https://app.example/cb");
592 assert!(!code.is_empty());
593 assert_eq!(state.as_deref(), Some("state-1"));
594 assert!(iss.contains("pod.example"));
595 }
596 other => panic!("expected Redirect, got {other:?}"),
597 }
598 }
599
600 #[tokio::test]
601 async fn authorize_rejects_unregistered_redirect_uri() {
602 let (p, _, client, verifier) = seed_provider().await;
603 let req = AuthorizeRequest {
604 client_id: client.client_id.clone(),
605 response_type: "code".into(),
606 redirect_uri: "https://evil.example/steal".into(),
607 state: None,
608 code_challenge: Some(s256(&verifier)),
609 code_challenge_method: Some("S256".into()),
610 scope: Some("openid".into()),
611 session_account_id: Some("acct-1".into()),
612 };
613 let err = p.authorize(req).await.unwrap_err();
614 assert!(matches!(err, ProviderError::InvalidRequest(_)));
615 }
616
617 #[tokio::test]
618 async fn authorize_rejects_without_pkce() {
619 let (p, _, client, _) = seed_provider().await;
620 let req = AuthorizeRequest {
621 client_id: client.client_id.clone(),
622 response_type: "code".into(),
623 redirect_uri: "https://app.example/cb".into(),
624 state: None,
625 code_challenge: None,
626 code_challenge_method: None,
627 scope: Some("openid".into()),
628 session_account_id: Some("acct-1".into()),
629 };
630 let err = p.authorize(req).await.unwrap_err();
631 assert!(matches!(err, ProviderError::InvalidRequest(_)));
632 }
633
634 #[tokio::test]
635 async fn token_endpoint_rejects_without_dpop() {
636 let (p, _, client, verifier) = seed_provider().await;
637 let auth = p
639 .authorize(AuthorizeRequest {
640 client_id: client.client_id.clone(),
641 response_type: "code".into(),
642 redirect_uri: "https://app.example/cb".into(),
643 state: None,
644 code_challenge: Some(s256(&verifier)),
645 code_challenge_method: Some("S256".into()),
646 scope: Some("openid webid".into()),
647 session_account_id: Some("acct-1".into()),
648 })
649 .await
650 .unwrap();
651 let code = match auth {
652 AuthorizeResponse::Redirect { code, .. } => code,
653 _ => panic!(),
654 };
655
656 let err = p
657 .token(TokenRequest {
658 grant_type: "authorization_code".into(),
659 code: &code,
660 redirect_uri: "https://app.example/cb".into(),
661 client_id: client.client_id.clone(),
662 code_verifier: Some(&verifier),
663 dpop_proof: None,
664 now_unix: 1_700_000_000,
665 })
666 .await
667 .unwrap_err();
668 assert!(matches!(err, ProviderError::InvalidDpop(_)));
669 }
670
671 #[tokio::test]
672 async fn token_endpoint_rejects_dpop_with_wrong_htu() {
673 let (p, _, client, verifier) = seed_provider().await;
674 let auth = p
675 .authorize(AuthorizeRequest {
676 client_id: client.client_id.clone(),
677 response_type: "code".into(),
678 redirect_uri: "https://app.example/cb".into(),
679 state: None,
680 code_challenge: Some(s256(&verifier)),
681 code_challenge_method: Some("S256".into()),
682 scope: Some("openid webid".into()),
683 session_account_id: Some("acct-1".into()),
684 })
685 .await
686 .unwrap();
687 let code = match auth {
688 AuthorizeResponse::Redirect { code, .. } => code,
689 _ => panic!(),
690 };
691
692 let wrong_htu = "https://evil.example/idp/token";
693 let proof = test_dpop_proof(wrong_htu, "POST", 1_700_000_000);
694 let err = p
695 .token(TokenRequest {
696 grant_type: "authorization_code".into(),
697 code: &code,
698 redirect_uri: "https://app.example/cb".into(),
699 client_id: client.client_id.clone(),
700 code_verifier: Some(&verifier),
701 dpop_proof: Some(&proof),
702 now_unix: 1_700_000_000,
703 })
704 .await
705 .unwrap_err();
706 assert!(matches!(err, ProviderError::InvalidDpop(_)));
707 }
708
709 #[tokio::test]
710 async fn authorization_code_flow_end_to_end() {
711 let (p, _, client, verifier) = seed_provider().await;
712
713 let auth = p
714 .authorize(AuthorizeRequest {
715 client_id: client.client_id.clone(),
716 response_type: "code".into(),
717 redirect_uri: "https://app.example/cb".into(),
718 state: Some("s-1".into()),
719 code_challenge: Some(s256(&verifier)),
720 code_challenge_method: Some("S256".into()),
721 scope: Some("openid webid".into()),
722 session_account_id: Some("acct-1".into()),
723 })
724 .await
725 .unwrap();
726 let code = match auth {
727 AuthorizeResponse::Redirect { code, .. } => code,
728 _ => panic!(),
729 };
730
731 let proof = test_dpop_proof("https://pod.example/idp/token", "POST", 1_700_000_000);
732 let tok = p
733 .token(TokenRequest {
734 grant_type: "authorization_code".into(),
735 code: &code,
736 redirect_uri: "https://app.example/cb".into(),
737 client_id: client.client_id.clone(),
738 code_verifier: Some(&verifier),
739 dpop_proof: Some(&proof),
740 now_unix: 1_700_000_000,
741 })
742 .await
743 .unwrap();
744 assert_eq!(tok.token_type, "DPoP");
745 assert!(tok.access_token.contains('.'));
746 assert_eq!(tok.expires_in, 3600);
747 assert_eq!(tok.webid.as_deref(), Some("https://alice.example/profile#me"));
748
749 let proof2 = test_dpop_proof("https://pod.example/idp/token", "POST", 1_700_000_000);
751 let err = p
752 .token(TokenRequest {
753 grant_type: "authorization_code".into(),
754 code: &code,
755 redirect_uri: "https://app.example/cb".into(),
756 client_id: client.client_id.clone(),
757 code_verifier: Some(&verifier),
758 dpop_proof: Some(&proof2),
759 now_unix: 1_700_000_000,
760 })
761 .await
762 .unwrap_err();
763 assert!(matches!(err, ProviderError::InvalidGrant(_)));
764 }
765
766 #[tokio::test]
767 async fn token_endpoint_rejects_pkce_verifier_mismatch() {
768 let (p, _, client, verifier) = seed_provider().await;
769 let auth = p
770 .authorize(AuthorizeRequest {
771 client_id: client.client_id.clone(),
772 response_type: "code".into(),
773 redirect_uri: "https://app.example/cb".into(),
774 state: None,
775 code_challenge: Some(s256(&verifier)),
776 code_challenge_method: Some("S256".into()),
777 scope: Some("openid webid".into()),
778 session_account_id: Some("acct-1".into()),
779 })
780 .await
781 .unwrap();
782 let code = match auth {
783 AuthorizeResponse::Redirect { code, .. } => code,
784 _ => panic!(),
785 };
786 let proof = test_dpop_proof("https://pod.example/idp/token", "POST", 1_700_000_000);
787 let err = p
788 .token(TokenRequest {
789 grant_type: "authorization_code".into(),
790 code: &code,
791 redirect_uri: "https://app.example/cb".into(),
792 client_id: client.client_id.clone(),
793 code_verifier: Some("totally-wrong-verifier"),
794 dpop_proof: Some(&proof),
795 now_unix: 1_700_000_000,
796 })
797 .await
798 .unwrap_err();
799 assert!(matches!(err, ProviderError::InvalidGrant(_)));
800 }
801}