1- use std:: time:: { Duration , SystemTime } ;
2-
1+ use anyhow:: anyhow;
32use axum:: extract:: { Query , Request , State } ;
43use axum:: middleware:: Next ;
54use axum:: response:: IntoResponse ;
65use axum_extra:: typed_header:: TypedHeader ;
76use headers:: { authorization, HeaderMapExt } ;
87use http:: { request, HeaderValue , StatusCode } ;
98use serde:: { Deserialize , Serialize } ;
10- use spacetimedb:: auth:: identity:: SpacetimeIdentityClaims ;
9+ use spacetimedb:: auth:: identity:: { ConnectionAuthCtx , SpacetimeIdentityClaims } ;
1110use spacetimedb:: auth:: identity:: { JwtError , JwtErrorKind } ;
1211use spacetimedb:: auth:: token_validation:: {
1312 new_validator, DefaultValidator , TokenSigner , TokenValidationError , TokenValidator ,
1413} ;
1514use spacetimedb:: auth:: JwtKeys ;
1615use spacetimedb:: energy:: EnergyQuanta ;
1716use spacetimedb:: identity:: Identity ;
17+ use std:: time:: { Duration , SystemTime } ;
1818use uuid:: Uuid ;
1919
2020use crate :: { log_and_500, ControlStateDelegate , NodeDelegate } ;
21+ use base64:: { engine:: general_purpose, Engine } ;
2122
2223/// Credentials for login for a spacetime identity, represented as a JWT.
2324///
@@ -41,6 +42,19 @@ impl SpacetimeCreds {
4142 Self { token }
4243 }
4344
45+ fn extract_jwt_payload_string ( & self ) -> Option < String > {
46+ let parts: Vec < & str > = self . token . split ( '.' ) . collect ( ) ;
47+ if parts. len ( ) != 3 {
48+ return None ;
49+ }
50+
51+ let payload_encoded = parts[ 1 ] ;
52+ let decoded_bytes = general_purpose:: URL_SAFE_NO_PAD . decode ( payload_encoded) . ok ( ) ?;
53+ let json_str = String :: from_utf8 ( decoded_bytes) . ok ( ) ?;
54+
55+ Some ( json_str)
56+ }
57+
4458 pub fn to_header_value ( & self ) -> HeaderValue {
4559 let mut val = HeaderValue :: try_from ( [ "Bearer " , self . token ( ) ] . concat ( ) ) . unwrap ( ) ;
4660 val. set_sensitive ( true ) ;
@@ -70,9 +84,31 @@ impl SpacetimeCreds {
7084#[ derive( Clone ) ]
7185pub struct SpacetimeAuth {
7286 pub creds : SpacetimeCreds ,
73- pub identity : Identity ,
74- pub subject : String ,
75- pub issuer : String ,
87+ pub claims : SpacetimeIdentityClaims ,
88+ /// The JWT payload as a json string (after base64 decoding).
89+ pub jwt_payload : String ,
90+ }
91+
92+ impl SpacetimeAuth {
93+ pub fn new ( creds : SpacetimeCreds , claims : SpacetimeIdentityClaims ) -> Result < Self , anyhow:: Error > {
94+ let payload = creds
95+ . extract_jwt_payload_string ( )
96+ . ok_or_else ( || anyhow ! ( "Failed to extract JWT payload" ) ) ?;
97+ Ok ( Self {
98+ creds,
99+ claims,
100+ jwt_payload : payload,
101+ } )
102+ }
103+ }
104+
105+ impl From < SpacetimeAuth > for ConnectionAuthCtx {
106+ fn from ( auth : SpacetimeAuth ) -> Self {
107+ ConnectionAuthCtx {
108+ claims : auth. claims ,
109+ jwt_payload : auth. jwt_payload . clone ( ) ,
110+ }
111+ }
76112}
77113
78114use jsonwebtoken;
@@ -84,10 +120,10 @@ pub struct TokenClaims {
84120}
85121
86122impl From < SpacetimeAuth > for TokenClaims {
87- fn from ( claims : SpacetimeAuth ) -> Self {
123+ fn from ( auth : SpacetimeAuth ) -> Self {
88124 Self {
89- issuer : claims. issuer ,
90- subject : claims. subject ,
125+ issuer : auth . claims . issuer ,
126+ subject : auth . claims . subject ,
91127 // This will need to be changed when we care about audiencies.
92128 audience : Vec :: new ( ) ,
93129 }
@@ -108,11 +144,14 @@ impl TokenClaims {
108144 Identity :: from_claims ( & self . issuer , & self . subject )
109145 }
110146
147+ /// Encode the claims into a JWT token and sign it with the provided signer.
148+ /// This also adds claims for expiry and issued at time.
149+ /// Returns an object representing the claims and the signed token.
111150 pub fn encode_and_sign_with_expiry (
112151 & self ,
113152 signer : & impl TokenSigner ,
114153 expiry : Option < Duration > ,
115- ) -> Result < String , JwtError > {
154+ ) -> Result < ( SpacetimeIdentityClaims , String ) , JwtError > {
116155 let iat = SystemTime :: now ( ) ;
117156 let exp = expiry. map ( |dur| iat + dur) ;
118157 let claims = SpacetimeIdentityClaims {
@@ -123,10 +162,14 @@ impl TokenClaims {
123162 iat,
124163 exp,
125164 } ;
126- signer. sign ( & claims)
165+ let token = signer. sign ( & claims) ?;
166+ Ok ( ( claims, token) )
127167 }
128168
129- pub fn encode_and_sign ( & self , signer : & impl TokenSigner ) -> Result < String , JwtError > {
169+ /// Encode the claims into a JWT token and sign it with the provided signer.
170+ /// This also adds a claim for issued at time.
171+ /// Returns an object representing the claims and the signed token.
172+ pub fn encode_and_sign ( & self , signer : & impl TokenSigner ) -> Result < ( SpacetimeIdentityClaims , String ) , JwtError > {
130173 self . encode_and_sign_with_expiry ( signer, None )
131174 }
132175}
@@ -143,32 +186,28 @@ impl SpacetimeAuth {
143186 audience : vec ! [ "spacetimedb" . to_string( ) ] ,
144187 } ;
145188
146- let identity = claims. id ( ) ;
147- let creds = {
148- let token = claims. encode_and_sign ( ctx. jwt_auth_provider ( ) ) . map_err ( log_and_500) ?;
149- SpacetimeCreds :: from_signed_token ( token)
150- } ;
151-
152- Ok ( Self {
153- creds,
154- identity,
155- subject,
156- issuer : ctx. jwt_auth_provider ( ) . local_issuer ( ) . to_string ( ) ,
157- } )
189+ let ( claims, token) = claims. encode_and_sign ( ctx. jwt_auth_provider ( ) ) . map_err ( log_and_500) ?;
190+ let creds = SpacetimeCreds :: from_signed_token ( token) ;
191+ // Pulling out the payload should never fail, since we just made it.
192+ Self :: new ( creds, claims) . map_err ( log_and_500)
158193 }
159194
160195 /// Get the auth credentials as headers to be returned from an endpoint.
161196 pub fn into_headers ( self ) -> ( TypedHeader < SpacetimeIdentity > , TypedHeader < SpacetimeIdentityToken > ) {
162197 (
163- TypedHeader ( SpacetimeIdentity ( self . identity ) ) ,
198+ TypedHeader ( SpacetimeIdentity ( self . claims . identity ) ) ,
164199 TypedHeader ( SpacetimeIdentityToken ( self . creds ) ) ,
165200 )
166201 }
167202
168203 // Sign a new token with the same claims and a new expiry.
169204 // Note that this will not change the issuer, so the private_key might not match.
170205 // We do this to create short-lived tokens that we will be able to verify.
171- pub fn re_sign_with_expiry ( & self , signer : & impl TokenSigner , expiry : Duration ) -> Result < String , JwtError > {
206+ pub fn re_sign_with_expiry (
207+ & self ,
208+ signer : & impl TokenSigner ,
209+ expiry : Duration ,
210+ ) -> Result < ( SpacetimeIdentityClaims , String ) , JwtError > {
172211 TokenClaims :: from ( self . clone ( ) ) . encode_and_sign_with_expiry ( signer, Some ( expiry) )
173212 }
174213}
@@ -237,9 +276,11 @@ impl<TV: TokenValidator + Send + Sync> JwtAuthProvider for JwtKeyAuthProvider<TV
237276
238277#[ cfg( test) ]
239278mod tests {
240- use crate :: auth:: TokenClaims ;
279+ use crate :: auth:: { SpacetimeCreds , TokenClaims } ;
241280 use anyhow:: Ok ;
281+
242282 use spacetimedb:: auth:: { token_validation:: TokenValidator , JwtKeys } ;
283+ use std:: collections:: HashSet ;
243284
244285 // Make sure that when we encode TokenClaims, we can decode to get the expected identity.
245286 #[ tokio:: test]
@@ -252,12 +293,48 @@ mod tests {
252293 audience : vec ! [ "spacetimedb" . to_string( ) ] ,
253294 } ;
254295 let id = claims. id ( ) ;
255- let token = claims. encode_and_sign ( & kp. private ) ?;
296+ let ( _ , token) = claims. encode_and_sign ( & kp. private ) ?;
256297 let decoded = kp. public . validate_token ( & token) . await ?;
257298
258299 assert_eq ! ( decoded. identity, id) ;
259300 Ok ( ( ) )
260301 }
302+
303+ // Test that extracting a JWT payload from a valid token gets the json representation.
304+ #[ tokio:: test]
305+ async fn extract_payload ( ) -> Result < ( ) , anyhow:: Error > {
306+ let kp = JwtKeys :: generate ( ) ?;
307+
308+ let dummy_audience = "spacetimedb" . to_string ( ) ;
309+ let claims = TokenClaims {
310+ issuer : "localhost" . to_string ( ) ,
311+ subject : "test-subject" . to_string ( ) ,
312+ audience : vec ! [ dummy_audience. clone( ) ] ,
313+ } ;
314+ let ( _, token) = claims. encode_and_sign ( & kp. private ) ?;
315+ let st_creds = SpacetimeCreds :: from_signed_token ( token) ;
316+ let payload = st_creds
317+ . extract_jwt_payload_string ( )
318+ . ok_or_else ( || anyhow:: anyhow!( "Failed to extract JWT payload" ) ) ?;
319+ // Make sure it is valid json.
320+ let parsed: serde_json:: Value = serde_json:: from_str ( & payload) ?;
321+ assert_eq ! ( parsed. get( "iss" ) . unwrap( ) . as_str( ) . unwrap( ) , claims. issuer) ;
322+ assert_eq ! ( parsed. get( "sub" ) . unwrap( ) . as_str( ) . unwrap( ) , claims. subject) ;
323+ assert_eq ! (
324+ parsed. get( "aud" ) . unwrap( ) . as_array( ) . unwrap( ) [ 0 ] . as_str( ) . unwrap( ) ,
325+ dummy_audience
326+ ) ;
327+ let as_object = parsed
328+ . as_object ( )
329+ . ok_or_else ( || anyhow:: anyhow!( "Failed to parse JWT payload as object" ) ) ?;
330+ let keys: HashSet < String > = as_object. keys ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
331+ let expected_keys = vec ! [ "iss" , "sub" , "aud" , "iat" , "exp" , "hex_identity" ]
332+ . into_iter ( )
333+ . map ( |s| s. to_string ( ) )
334+ . collect :: < HashSet < String > > ( ) ;
335+ assert_eq ! ( keys, expected_keys) ;
336+ Ok ( ( ) )
337+ }
261338}
262339
263340pub async fn validate_token < S : NodeDelegate > (
@@ -283,11 +360,13 @@ impl<S: NodeDelegate + Send + Sync> axum::extract::FromRequestParts<S> for Space
283360 . await
284361 . map_err ( AuthorizationRejection :: Custom ) ?;
285362
363+ let payload = creds. extract_jwt_payload_string ( ) . ok_or_else ( || {
364+ AuthorizationRejection :: Custom ( TokenValidationError :: Other ( anyhow ! ( "Internal error parsing token" ) ) )
365+ } ) ?;
286366 let auth = SpacetimeAuth {
287367 creds,
288- identity : claims. identity ,
289- subject : claims. subject ,
290- issuer : claims. issuer ,
368+ claims,
369+ jwt_payload : payload,
291370 } ;
292371 Ok ( Self { auth : Some ( auth) } )
293372 }
0 commit comments