@@ -50,17 +50,34 @@ pub struct Authenticator {
5050
5151#[ derive( Debug , Serialize , Deserialize ) ]
5252pub struct Claims {
53- aud : Vec < String > ,
54- email : String ,
53+ aud : Audience ,
54+ email : Option < String > ,
55+ common_name : Option < String > ,
5556 exp : usize ,
5657 iat : usize ,
57- nbf : usize ,
58+ nbf : Option < usize > ,
5859 iss : String ,
5960 #[ serde( rename = "type" ) ] // use is a reserved keyword in Rust, so we will rename it
6061 type_ : String ,
61- identity_nonce : String ,
62+ identity_nonce : Option < String > ,
6263 sub : String ,
63- country : String ,
64+ country : Option < String > ,
65+ }
66+
67+ #[ derive( Debug , Serialize , Deserialize ) ]
68+ #[ serde( untagged) ]
69+ pub enum Audience {
70+ One ( String ) ,
71+ Many ( Vec < String > ) ,
72+ }
73+
74+ impl Audience {
75+ pub fn as_slice ( & self ) -> & [ String ] {
76+ match self {
77+ Self :: One ( audience) => std:: slice:: from_ref ( audience) ,
78+ Self :: Many ( audiences) => audiences,
79+ }
80+ }
6481}
6582
6683#[ derive( Error , Debug ) ]
@@ -75,6 +92,8 @@ pub enum ValidationError {
7592 InvalidToken ,
7693 #[ error( "Token is not a Cloudflare Access application token" ) ]
7794 InvalidTokenType ,
95+ #[ error( "Token has no user or service identity" ) ]
96+ MissingIdentity ,
7897 #[ error( "No matching AUD found" ) ]
7998 NoAudMatch ,
8099 #[ error( "Certificate not found" ) ]
@@ -107,7 +126,7 @@ impl ValidationError {
107126 | Self :: InvalidSigningKey
108127 | Self :: FetchCertificatesFailed => "signing_key_unavailable" ,
109128 Self :: NoAudMatch => "audience_mismatch" ,
110- Self :: InvalidToken => "malformed_token" ,
129+ Self :: InvalidToken | Self :: MissingIdentity => "malformed_token" ,
111130 Self :: InvalidTokenType => "invalid_token_type" ,
112131 Self :: JwtDecodingError ( error) => match error. kind ( ) {
113132 ErrorKind :: ExpiredSignature => "expired_token" ,
@@ -176,6 +195,9 @@ impl Authenticator {
176195 if token_data. claims . type_ != ACCESS_APPLICATION_TOKEN_TYPE {
177196 return Err ( ValidationError :: InvalidTokenType ) ;
178197 }
198+ if !token_data. claims . has_identity ( ) {
199+ return Err ( ValidationError :: MissingIdentity ) ;
200+ }
179201
180202 Ok ( token_data)
181203 }
@@ -252,6 +274,15 @@ impl Authenticator {
252274 }
253275}
254276
277+ impl Claims {
278+ fn has_identity ( & self ) -> bool {
279+ [ & self . email , & self . common_name ]
280+ . into_iter ( )
281+ . flatten ( )
282+ . any ( |identity| !identity. trim ( ) . is_empty ( ) )
283+ }
284+ }
285+
255286#[ cfg( test) ]
256287mod tests {
257288 use super :: * ;
@@ -323,16 +354,17 @@ mod tests {
323354 // Issued an hour before it expires, so expired tokens stay coherent.
324355 let issued = exp. saturating_sub ( 3600 ) ;
325356 Claims {
326- aud : vec ! [ aud. to_string( ) ] ,
327- email : "user@example.com" . to_string ( ) ,
357+ aud : Audience :: Many ( vec ! [ aud. to_string( ) ] ) ,
358+ email : Some ( "user@example.com" . to_string ( ) ) ,
359+ common_name : None ,
328360 exp,
329361 iat : issued,
330- nbf : issued,
362+ nbf : Some ( issued) ,
331363 iss : issuer. to_string ( ) ,
332364 type_ : ACCESS_APPLICATION_TOKEN_TYPE . to_string ( ) ,
333- identity_nonce : "test-nonce" . to_string ( ) ,
365+ identity_nonce : Some ( "test-nonce" . to_string ( ) ) ,
334366 sub : "00000000-0000-0000-0000-000000000000" . to_string ( ) ,
335- country : "SG" . to_string ( ) ,
367+ country : Some ( "SG" . to_string ( ) ) ,
336368 }
337369 }
338370
@@ -358,7 +390,7 @@ mod tests {
358390 }
359391
360392 #[ tokio:: test]
361- async fn accepts_a_validly_signed_token ( ) {
393+ async fn accepts_a_user_identity_with_an_array_audience ( ) {
362394 let mut server = mockito:: Server :: new_async ( ) . await ;
363395 let auth = authenticator ( & mut server, jwk ( KID , "RS256" ) ) . await ;
364396
@@ -372,8 +404,81 @@ mod tests {
372404 . await
373405 . expect ( "a correctly signed, unexpired, correctly-audienced token must verify" ) ;
374406
375- assert_eq ! ( decoded. claims. email, "user@example.com" ) ;
376- assert_eq ! ( decoded. claims. aud, vec![ AUD . to_string( ) ] ) ;
407+ assert_eq ! ( decoded. claims. email. as_deref( ) , Some ( "user@example.com" ) ) ;
408+ assert_eq ! ( decoded. claims. aud. as_slice( ) , & [ AUD . to_string( ) ] ) ;
409+ }
410+
411+ #[ tokio:: test]
412+ async fn accepts_a_service_identity_with_a_scalar_audience_and_no_nbf ( ) {
413+ let mut server = mockito:: Server :: new_async ( ) . await ;
414+ let auth = authenticator ( & mut server, jwk ( KID , "RS256" ) ) . await ;
415+ let mut payload = serde_json:: to_value ( claims ( AUD , now ( ) + 3600 , & server. url ( ) ) )
416+ . expect ( "serialize claims" ) ;
417+ let payload = payload
418+ . as_object_mut ( )
419+ . expect ( "claims serialize as an object" ) ;
420+ for claim in [ "email" , "nbf" , "identity_nonce" , "country" ] {
421+ payload. remove ( claim) ;
422+ }
423+ payload. insert ( "aud" . into ( ) , json ! ( AUD ) ) ;
424+ payload. insert ( "common_name" . into ( ) , json ! ( "automation-client" ) ) ;
425+ let token = sign_payload ( & payload, Algorithm :: RS256 , Some ( KID ) ) ;
426+
427+ let decoded = auth
428+ . decode ( & token, vec ! [ AUD . to_string( ) ] )
429+ . await
430+ . expect ( "a service-token identity should be accepted" ) ;
431+
432+ assert_eq ! ( decoded. claims. email, None ) ;
433+ assert_eq ! (
434+ decoded. claims. common_name. as_deref( ) ,
435+ Some ( "automation-client" )
436+ ) ;
437+ assert_eq ! ( decoded. claims. nbf, None ) ;
438+ assert_eq ! ( decoded. claims. aud. as_slice( ) , & [ AUD . to_string( ) ] ) ;
439+ }
440+
441+ #[ tokio:: test]
442+ async fn accepts_a_user_identity_with_a_scalar_audience ( ) {
443+ let mut server = mockito:: Server :: new_async ( ) . await ;
444+ let auth = authenticator ( & mut server, jwk ( KID , "RS256" ) ) . await ;
445+ let mut payload = serde_json:: to_value ( claims ( AUD , now ( ) + 3600 , & server. url ( ) ) )
446+ . expect ( "serialize claims" ) ;
447+ payload
448+ . as_object_mut ( )
449+ . expect ( "claims serialize as an object" )
450+ . insert ( "aud" . into ( ) , json ! ( AUD ) ) ;
451+ let token = sign_payload ( & payload, Algorithm :: RS256 , Some ( KID ) ) ;
452+
453+ let decoded = auth
454+ . decode ( & token, vec ! [ AUD . to_string( ) ] )
455+ . await
456+ . expect ( "a scalar audience with a user identity should be accepted" ) ;
457+
458+ assert_eq ! ( decoded. claims. email. as_deref( ) , Some ( "user@example.com" ) ) ;
459+ assert_eq ! ( decoded. claims. aud. as_slice( ) , & [ AUD . to_string( ) ] ) ;
460+ }
461+
462+ #[ tokio:: test]
463+ async fn rejects_an_application_token_without_a_user_or_service_identity ( ) {
464+ let mut server = mockito:: Server :: new_async ( ) . await ;
465+ let auth = authenticator ( & mut server, jwk ( KID , "RS256" ) ) . await ;
466+ let mut payload = serde_json:: to_value ( claims ( AUD , now ( ) + 3600 , & server. url ( ) ) )
467+ . expect ( "serialize claims" ) ;
468+ let payload = payload
469+ . as_object_mut ( )
470+ . expect ( "claims serialize as an object" ) ;
471+ payload. remove ( "email" ) ;
472+ payload. remove ( "common_name" ) ;
473+ let token = sign_payload ( & payload, Algorithm :: RS256 , Some ( KID ) ) ;
474+
475+ let error = auth
476+ . decode ( & token, vec ! [ AUD . to_string( ) ] )
477+ . await
478+ . expect_err ( "a token without an identity must fail closed" ) ;
479+
480+ assert ! ( matches!( error, ValidationError :: MissingIdentity ) ) ;
481+ assert_eq ! ( error. reason_code( ) , "malformed_token" ) ;
377482 }
378483
379484 #[ tokio:: test]
@@ -598,7 +703,7 @@ mod tests {
598703 let mut server = mockito:: Server :: new_async ( ) . await ;
599704 let auth = authenticator ( & mut server, jwk ( KID , "RS256" ) ) . await ;
600705 let mut token_claims = claims ( AUD , now ( ) + 3600 , & server. url ( ) ) ;
601- token_claims. nbf = now ( ) + 120 ;
706+ token_claims. nbf = Some ( now ( ) + 120 ) ;
602707 let token = sign ( & token_claims, Algorithm :: RS256 , Some ( KID ) ) ;
603708
604709 let error = auth
@@ -633,6 +738,30 @@ mod tests {
633738 assert_eq ! ( err. reason_code( ) , "audience_mismatch" ) ;
634739 }
635740
741+ #[ tokio:: test]
742+ async fn rejects_a_scalar_audience_that_is_not_in_the_catalog ( ) {
743+ let mut server = mockito:: Server :: new_async ( ) . await ;
744+ let auth = authenticator ( & mut server, jwk ( KID , "RS256" ) ) . await ;
745+ let mut payload =
746+ serde_json:: to_value ( claims ( "someone-elses-aud" , now ( ) + 3600 , & server. url ( ) ) )
747+ . expect ( "serialize claims" ) ;
748+ payload
749+ . as_object_mut ( )
750+ . expect ( "claims serialize as an object" )
751+ . insert ( "aud" . into ( ) , json ! ( "someone-elses-aud" ) ) ;
752+ let token = sign_payload ( & payload, Algorithm :: RS256 , Some ( KID ) ) ;
753+
754+ let error = auth
755+ . decode ( & token, vec ! [ AUD . to_string( ) ] )
756+ . await
757+ . expect_err ( "a non-matching scalar audience must fail closed" ) ;
758+
759+ assert ! (
760+ matches!( & error, ValidationError :: JwtDecodingError ( error) if matches!( error. kind( ) , ErrorKind :: InvalidAudience ) )
761+ ) ;
762+ assert_eq ! ( error. reason_code( ) , "audience_mismatch" ) ;
763+ }
764+
636765 #[ tokio:: test]
637766 async fn rejects_a_token_signed_by_an_unknown_key ( ) {
638767 let mut server = mockito:: Server :: new_async ( ) . await ;
0 commit comments