forked from
oppi.li/at-advent
this repo has no description
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}