@@ -12,12 +12,13 @@ pub mod rt;
1212#[ doc( hidden) ]
1313pub mod table;
1414
15- use spacetimedb_lib:: bsatn;
16- use std:: cell:: RefCell ;
17-
1815pub use log;
1916#[ cfg( feature = "rand" ) ]
2017pub use rand08 as rand;
18+ use spacetimedb_lib:: bsatn;
19+ use std:: cell:: { OnceCell , RefCell } ;
20+ use std:: ops:: Deref ;
21+ use std:: sync:: LazyLock ;
2122
2223#[ cfg( feature = "unstable" ) ]
2324pub use client_visibility_filter:: Filter ;
@@ -732,6 +733,8 @@ pub struct ReducerContext {
732733 /// See the [`#[table]`](macro@crate::table) macro for more information.
733734 pub db : Local ,
734735
736+ sender_auth : AuthCtx ,
737+
735738 #[ cfg( feature = "rand08" ) ]
736739 rng : std:: cell:: OnceCell < StdbRng > ,
737740}
@@ -744,6 +747,24 @@ impl ReducerContext {
744747 sender : Identity :: __dummy ( ) ,
745748 timestamp : Timestamp :: UNIX_EPOCH ,
746749 connection_id : None ,
750+ sender_auth : AuthCtx :: internal ( ) ,
751+ rng : std:: cell:: OnceCell :: new ( ) ,
752+ }
753+ }
754+
755+ #[ doc( hidden) ]
756+ fn new ( db : Local , sender : Identity , connection_id : Option < ConnectionId > , timestamp : Timestamp ) -> Self {
757+ let sender_auth = match connection_id {
758+ Some ( cid) => AuthCtx :: from_connection_id ( cid) ,
759+ None => AuthCtx :: internal ( ) ,
760+ } ;
761+ Self {
762+ db,
763+ sender,
764+ timestamp,
765+ connection_id,
766+ sender_auth,
767+ #[ cfg( feature = "rand08" ) ]
747768 rng : std:: cell:: OnceCell :: new ( ) ,
748769 }
749770 }
@@ -796,6 +817,114 @@ impl DbContext for ReducerContext {
796817#[ non_exhaustive]
797818pub struct Local { }
798819
820+ #[ non_exhaustive]
821+ pub struct JwtClaims {
822+ payload : String ,
823+ parsed : OnceCell < serde_json:: Value > ,
824+ audience : OnceCell < Vec < String > > ,
825+ }
826+
827+ /// Authentication information for the caller of a reducer.
828+ pub struct AuthCtx {
829+ is_internal : bool ,
830+ // I can't directly use a LazyLock without making this struct generic.
831+ jwt : Box < dyn Deref < Target = Option < JwtClaims > > > ,
832+ }
833+
834+ impl AuthCtx {
835+ fn new < F > ( is_internal : bool , jwt_fn : F ) -> Self
836+ where
837+ F : FnOnce ( ) -> Option < JwtClaims > + ' static ,
838+ {
839+ AuthCtx {
840+ is_internal,
841+ jwt : Box :: new ( LazyLock :: new ( jwt_fn) ) ,
842+ }
843+ }
844+
845+ /// Create an AuthCtx for an internal call, with no JWT.
846+ /// This represents a scheduled reducer.
847+ pub fn internal ( ) -> AuthCtx {
848+ Self :: new ( true , || None )
849+ }
850+
851+ /// Create an AuthCtx using the json claims from a JWT.
852+ /// This can be used to write unit tests.
853+ pub fn from_jwt_payload ( jwt_payload : String ) -> AuthCtx {
854+ Self :: new ( false , move || Some ( JwtClaims :: new ( jwt_payload) ) )
855+ }
856+
857+ /// Create an AuthCtx that reads the JWT for the given connection id.
858+ fn from_connection_id ( connection_id : ConnectionId ) -> AuthCtx {
859+ Self :: new ( false , move || {
860+ spacetimedb_bindings_sys:: get_jwt ( connection_id. as_le_byte_array ( ) ) . map ( JwtClaims :: new)
861+ } )
862+ }
863+
864+ /// True if this reducer was spawned from inside the database.
865+ pub fn is_internal ( & self ) -> bool {
866+ self . is_internal
867+ }
868+
869+ /// Check if there is a JWT without loading it.
870+ /// If is_internal is true, this will be false.
871+ pub fn has_jwt ( & self ) -> bool {
872+ self . jwt . is_some ( )
873+ }
874+
875+ /// Load the jwt.
876+ pub fn jwt ( & self ) -> Option < & JwtClaims > {
877+ self . jwt . as_ref ( ) . deref ( ) . as_ref ( )
878+ }
879+ }
880+
881+ impl JwtClaims {
882+ fn new ( jwt : String ) -> Self {
883+ Self {
884+ payload : jwt,
885+ parsed : OnceCell :: new ( ) ,
886+ audience : OnceCell :: new ( ) ,
887+ }
888+ }
889+
890+ fn get_parsed ( & self ) -> & serde_json:: Value {
891+ self . parsed
892+ . get_or_init ( || serde_json:: from_str ( & self . payload ) . expect ( "Failed to parse JWT payload" ) )
893+ }
894+
895+ pub fn subject ( & self ) -> & str {
896+ // TODO: Add more error messages here.
897+ self . get_parsed ( ) . get ( "sub" ) . unwrap ( ) . as_str ( ) . unwrap ( )
898+ }
899+
900+ pub fn issuer ( & self ) -> & str {
901+ self . get_parsed ( ) . get ( "iss" ) . unwrap ( ) . as_str ( ) . unwrap ( )
902+ }
903+
904+ fn extract_audience ( & self ) -> Vec < String > {
905+ let aud = self . get_parsed ( ) . get ( "aud" ) . unwrap ( ) ;
906+ match aud {
907+ serde_json:: Value :: String ( s) => vec ! [ s. clone( ) ] ,
908+ serde_json:: Value :: Array ( arr) => arr. iter ( ) . filter_map ( |v| v. as_str ( ) . map ( String :: from) ) . collect ( ) ,
909+ _ => panic ! ( "Unexpected type for 'aud' claim in JWT" ) ,
910+ }
911+ }
912+
913+ pub fn audience ( & self ) -> & [ String ] {
914+ self . audience . get_or_init ( || self . extract_audience ( ) )
915+ }
916+
917+ // A convenience method, since this may not be in the token.
918+ pub fn identity ( & self ) -> Identity {
919+ Identity :: from_claims ( self . issuer ( ) , self . subject ( ) )
920+ }
921+
922+ // We can expose the whole payload for users that want to parse custom claims.
923+ pub fn raw_payload ( & self ) -> & str {
924+ & self . payload
925+ }
926+ }
927+
799928// #[cfg(target_arch = "wasm32")]
800929// #[global_allocator]
801930// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
@@ -900,3 +1029,37 @@ macro_rules! __volatile_nonatomic_schedule_immediate_impl {
9001029 }
9011030 } ;
9021031}
1032+
1033+ #[ cfg( test) ]
1034+ mod tests {
1035+ use super :: * ;
1036+
1037+ #[ test]
1038+ fn parse_single_audience ( ) {
1039+ let example_payload = r#"
1040+ {
1041+ "iss": "https://securetoken.google.com/my-project-id",
1042+ "aud": "my-project-id",
1043+ "auth_time": 1695560000,
1044+ "user_id": "abc123XYZ",
1045+ "sub": "abc123XYZ",
1046+ "iat": 1695560100,
1047+ "exp": 1695563700,
1048+ "email": "user@example.com",
1049+ "email_verified": true,
1050+ "firebase": {
1051+ "identities": {
1052+ "email": ["user@example.com"]
1053+ },
1054+ "sign_in_provider": "password"
1055+ },
1056+ "name": "Jane Doe",
1057+ "picture": "https://lh3.googleusercontent.com/a-/profile.jpg"
1058+ }
1059+ "# ;
1060+ let auth = AuthCtx :: from_jwt_payload ( example_payload. to_string ( ) ) ;
1061+ let audience = auth. jwt ( ) . unwrap ( ) . audience ( ) ;
1062+ assert_eq ! ( audience. len( ) , 1 ) ;
1063+ assert_eq ! ( audience, & [ "my-project-id" . to_string( ) ] ) ;
1064+ }
1065+ }
0 commit comments