this repo has no description
at main 193 lines 6.5 kB view raw
1use crate::HandleResolver; 2use crate::OAuthAgentType; 3use crate::advent::day::Day; 4use crate::advent::{AdventChallenge, AdventError, AdventPart, ChallengeCheckResponse}; 5use crate::atrium::service_auth::{ 6 decode_and_verify_service_auth, decode_jwt_claims, extract_signing_key_bytes, 7}; 8use async_trait::async_trait; 9use atrium_api::types::string::Did; 10use atrium_common::resolver::Resolver; 11use serde_json::json; 12use sqlx::PgPool; 13 14pub const LXM_PART_ONE: &str = "codes.advent.challenge.exampleEndpoint"; 15pub const LXM_PART_TWO: &str = "codes.advent.challenge.getCode"; 16 17pub struct DaySix { 18 pub pool: PgPool, 19 pub oauth_client: Option<OAuthAgentType>, 20 pub handle_resolver: HandleResolver, 21 pub service_did: String, 22} 23 24#[async_trait] 25impl AdventChallenge for DaySix { 26 fn pool(&self) -> &PgPool { 27 &self.pool 28 } 29 30 fn day(&self) -> Day { 31 Day::Six 32 } 33 34 fn has_part_two(&self) -> bool { 35 true 36 } 37 38 fn requires_manual_verification_part_one(&self) -> bool { 39 true 40 } 41 42 fn requires_manual_verification_part_two(&self) -> bool { 43 true 44 } 45 46 async fn build_additional_context( 47 &self, 48 _did: &str, 49 _part: &AdventPart, 50 _code: &str, 51 ) -> Result<Option<serde_json::Value>, AdventError> { 52 // Strip "did:web:" prefix for the domain used in URLs 53 let domain = self 54 .service_did 55 .strip_prefix("did:web:") 56 .unwrap_or(&self.service_did); 57 Ok(Some(json!({ 58 "service_did": self.service_did, 59 "service_did_domain": domain, 60 "lxm_part_one": LXM_PART_ONE, 61 "lxm_part_two": LXM_PART_TWO, 62 }))) 63 } 64 65 async fn check_part_one( 66 &self, 67 did: String, 68 verification_code: Option<String>, 69 ) -> Result<ChallengeCheckResponse, AdventError> { 70 let jwt = match verification_code { 71 Some(code) if !code.trim().is_empty() => code.trim().to_string(), 72 _ => { 73 return Ok(ChallengeCheckResponse::Incorrect( 74 "Please paste your serviceAuth JWT".to_string(), 75 )); 76 } 77 }; 78 79 // Decode claims first (without signature verification) to get the issuer 80 let claims = match decode_jwt_claims(&jwt) { 81 Ok(claims) => claims, 82 Err(e) => { 83 return Ok(ChallengeCheckResponse::Incorrect(format!( 84 "Could not decode JWT: {e}" 85 ))); 86 } 87 }; 88 89 // Verify the issuer matches the logged-in user 90 if claims.iss != did { 91 return Ok(ChallengeCheckResponse::Incorrect(format!( 92 "The JWT issuer (iss) '{}' does not match your DID '{}'", 93 claims.iss, did 94 ))); 95 } 96 97 // Check aud 98 if claims.aud != self.service_did { 99 return Ok(ChallengeCheckResponse::Incorrect(format!( 100 "The JWT audience (aud) '{}' does not match the expected value '{}'", 101 claims.aud, self.service_did 102 ))); 103 } 104 105 // Check lxm 106 match &claims.lxm { 107 Some(lxm) if lxm == LXM_PART_ONE => {} 108 Some(lxm) => { 109 return Ok(ChallengeCheckResponse::Incorrect(format!( 110 "The JWT lexicon method (lxm) '{}' does not match the expected value '{}'", 111 lxm, LXM_PART_ONE 112 ))); 113 } 114 None => { 115 return Ok(ChallengeCheckResponse::Incorrect( 116 "The JWT is missing the lexicon method (lxm) claim".to_string(), 117 )); 118 } 119 } 120 121 // Resolve DID document and extract signing key 122 let iss_did: Did = claims.iss.parse().map_err(|_| { 123 AdventError::ShouldNotHappen(format!("Invalid DID in JWT iss: {}", claims.iss)) 124 })?; 125 let did_doc = self 126 .handle_resolver 127 .resolve(&iss_did) 128 .await 129 .map_err(|err| { 130 log::error!("Failed to resolve DID document for {}: {}", claims.iss, err); 131 AdventError::ShouldNotHappen(format!("Failed to resolve DID document: {err}")) 132 })?; 133 134 let (key_alg, key_bytes) = match extract_signing_key_bytes(&did_doc) { 135 Ok(result) => result, 136 Err(e) => { 137 return Ok(ChallengeCheckResponse::Incorrect(format!( 138 "Could not extract signing key from your DID document: {e}" 139 ))); 140 } 141 }; 142 143 // Fully verify the JWT signature 144 match decode_and_verify_service_auth(&jwt, &key_bytes, key_alg) { 145 Ok(_) => Ok(ChallengeCheckResponse::Correct), 146 Err(e) => Ok(ChallengeCheckResponse::Incorrect(format!( 147 "JWT verification failed: {e}" 148 ))), 149 } 150 } 151 152 async fn check_part_two( 153 &self, 154 did: String, 155 verification_code: Option<String>, 156 ) -> Result<ChallengeCheckResponse, AdventError> { 157 let submitted_code = match verification_code { 158 Some(code) if !code.trim().is_empty() => code.trim().to_string(), 159 _ => { 160 return Ok(ChallengeCheckResponse::Incorrect( 161 "Please enter the verification code from the XRPC response".to_string(), 162 )); 163 } 164 }; 165 166 let challenge = self.get_days_challenge(&did).await?; 167 match challenge { 168 None => { 169 log::error!("No challenge record found for day 6 for user: {did}"); 170 Err(AdventError::ShouldNotHappen( 171 "Could not find challenge record".to_string(), 172 )) 173 } 174 Some(challenge) => { 175 let expected = 176 challenge 177 .verification_code_two 178 .ok_or(AdventError::ShouldNotHappen( 179 "No verification code for part two".to_string(), 180 ))?; 181 182 if submitted_code == expected { 183 Ok(ChallengeCheckResponse::Correct) 184 } else { 185 Ok(ChallengeCheckResponse::Incorrect(format!( 186 "The code '{}' is incorrect. Make sure you're using the code from the XRPC response.", 187 submitted_code 188 ))) 189 } 190 } 191 } 192 } 193}