@@ -25,6 +25,17 @@ pub fn peek_jwt_typ(token: &str) -> Option<String> {
2525 header[ "typ" ] . as_str ( ) . map ( |s| s. to_string ( ) )
2626}
2727
28+ /// Peek at a delegation token's payload to extract the `sub` (space URI) without verifying.
29+ pub fn peek_delegation_sub ( token : & str ) -> Option < String > {
30+ let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
31+ if parts. len ( ) != 3 {
32+ return None ;
33+ }
34+ let payload_bytes = URL_SAFE_NO_PAD . decode ( parts[ 1 ] ) . ok ( ) ?;
35+ let claims: DelegationTokenClaims = serde_json:: from_slice ( & payload_bytes) . ok ( ) ?;
36+ Some ( claims. sub )
37+ }
38+
2839/// Peek at a space credential JWT's payload to extract the `sub` (space URI) without verifying.
2940pub fn peek_credential_sub ( token : & str ) -> Option < String > {
3041 let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
@@ -69,6 +80,7 @@ pub fn sign_delegation_token(
6980pub fn verify_delegation_token (
7081 token : & str ,
7182 verifying_key : & K256VerifyingKey ,
83+ expected_aud : & str ,
7284) -> Result < DelegationTokenClaims , AppError > {
7385 let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
7486 if parts. len ( ) != 3 {
@@ -132,6 +144,12 @@ pub fn verify_delegation_token(
132144 return Err ( AppError :: Auth ( "delegation token has expired" . into ( ) ) ) ;
133145 }
134146
147+ if claims. aud != expected_aud {
148+ return Err ( AppError :: Auth (
149+ "delegation token audience does not match this host" . into ( ) ,
150+ ) ) ;
151+ }
152+
135153 Ok ( claims)
136154}
137155
@@ -448,7 +466,7 @@ mod tests {
448466 let claims = make_delegation_claims ( ) ;
449467
450468 let token = sign_delegation_token ( & claims, & signing_key) . unwrap ( ) ;
451- let verified = verify_delegation_token ( & token, & verifying_key) . unwrap ( ) ;
469+ let verified = verify_delegation_token ( & token, & verifying_key, & claims . aud ) . unwrap ( ) ;
452470
453471 assert_eq ! ( verified. iss, claims. iss) ;
454472 assert_eq ! ( verified. sub, claims. sub) ;
@@ -464,8 +482,21 @@ mod tests {
464482 let claims = make_delegation_claims ( ) ;
465483
466484 let token = sign_delegation_token ( & claims, & signing_key) . unwrap ( ) ;
467- let result = verify_delegation_token ( & token, & verifying_key) ;
485+ let result = verify_delegation_token ( & token, & verifying_key, & claims. aud ) ;
486+ assert ! ( result. is_err( ) ) ;
487+ }
488+
489+ #[ test]
490+ fn delegation_rejects_wrong_aud ( ) {
491+ let signing_key = make_k256_signing_key ( ) ;
492+ let verifying_key = K256VerifyingKey :: from ( & signing_key) ;
493+ let claims = make_delegation_claims ( ) ;
494+
495+ let token = sign_delegation_token ( & claims, & signing_key) . unwrap ( ) ;
496+ let result =
497+ verify_delegation_token ( & token, & verifying_key, "did:plc:wrong#atproto_space_host" ) ;
468498 assert ! ( result. is_err( ) ) ;
499+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "audience" ) ) ;
469500 }
470501
471502 #[ test]
@@ -486,7 +517,7 @@ mod tests {
486517 } ;
487518
488519 let token = sign_delegation_token ( & claims, & signing_key) . unwrap ( ) ;
489- let result = verify_delegation_token ( & token, & verifying_key) ;
520+ let result = verify_delegation_token ( & token, & verifying_key, & claims . aud ) ;
490521 assert ! ( result. is_err( ) ) ;
491522 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "expired" ) ) ;
492523 }
@@ -510,7 +541,7 @@ mod tests {
510541 URL_SAFE_NO_PAD . encode( sig. to_bytes( ) )
511542 ) ;
512543
513- let result = verify_delegation_token ( & token, & verifying_key) ;
544+ let result = verify_delegation_token ( & token, & verifying_key, & claims . aud ) ;
514545 assert ! ( result. is_err( ) ) ;
515546 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "typ" ) ) ;
516547 }
0 commit comments