@@ -20,7 +20,7 @@ use super::JwtKeys;
2020
2121#[ derive( thiserror:: Error , Debug ) ]
2222pub enum TokenValidationError {
23- // TODO: Add real error types .
23+ // TODO: Replace the remaining dependency-specific and catch-all variants with domain errors .
2424
2525 // TODO: If we had our own errors defined we wouldn't be locked into this lib.
2626 #[ error( "Invalid token: {0}" ) ]
@@ -29,15 +29,50 @@ pub enum TokenValidationError {
2929 #[ error( "Specified key ID not found in JWKs" ) ]
3030 KeyIDNotFound ,
3131
32+ /// The token was decoded, but its claims are not acceptable.
33+ #[ error( transparent) ]
34+ InvalidClaims ( anyhow:: Error ) ,
35+
3236 #[ error( transparent) ]
3337 JwkError ( #[ from] jwks:: JwkError ) ,
3438 #[ error( transparent) ]
3539 JwksError ( #[ from] jwks:: JwksError ) ,
40+
41+ /// The identity provider's validation material could not be obtained.
42+ #[ error( transparent) ]
43+ IdentityProviderUnavailable ( anyhow:: Error ) ,
44+
3645 // The other case is a catch-all for unexpected errors.
3746 #[ error( transparent) ]
3847 Other ( #[ from] anyhow:: Error ) ,
3948}
4049
50+ /// The operational category of a token validation failure.
51+ #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
52+ pub enum TokenValidationErrorCategory {
53+ /// The supplied token is invalid and the client may retry with different credentials.
54+ InvalidCredentials ,
55+ /// Validation failed because identity-provider data could not be obtained or used.
56+ IdentityProvider ,
57+ /// Validation failed because of an unexpected local condition.
58+ Internal ,
59+ }
60+
61+ impl TokenValidationError {
62+ /// Classifies this failure for operational handling such as log severity.
63+ pub fn category ( & self ) -> TokenValidationErrorCategory {
64+ match self {
65+ Self :: TokenError ( _) | Self :: KeyIDNotFound | Self :: InvalidClaims ( _) => {
66+ TokenValidationErrorCategory :: InvalidCredentials
67+ }
68+ Self :: JwkError ( _) | Self :: JwksError ( _) | Self :: IdentityProviderUnavailable ( _) => {
69+ TokenValidationErrorCategory :: IdentityProvider
70+ }
71+ Self :: Other ( _) => TokenValidationErrorCategory :: Internal ,
72+ }
73+ }
74+ }
75+
4176// A token signer is responsible for signing tokens without doing any validation.
4277pub trait TokenSigner : Sync + Send {
4378 // Serialize the given claims and sign a JWT token with them as the payload.
@@ -158,7 +193,7 @@ impl TokenValidator for DecodingKey {
158193
159194 let data = decode :: < IncomingClaims > ( token, self , & validation) ?;
160195 let claims = data. claims ;
161- claims. try_into ( ) . map_err ( TokenValidationError :: Other )
196+ claims. try_into ( ) . map_err ( TokenValidationError :: InvalidClaims )
162197 }
163198}
164199
@@ -170,7 +205,7 @@ impl TokenValidator for BasicTokenValidator {
170205 if let Some ( expected_issuer) = & self . issuer
171206 && * claims. issuer != * * expected_issuer
172207 {
173- return Err ( TokenValidationError :: Other ( anyhow:: anyhow!(
208+ return Err ( TokenValidationError :: InvalidClaims ( anyhow:: anyhow!(
174209 "Issuer mismatch: got {:?}, expected {:?}" ,
175210 claims. issuer,
176211 expected_issuer
@@ -233,7 +268,11 @@ impl TokenValidator for CachingOidcTokenValidator {
233268 . cache
234269 . get ( String :: from ( raw_issuer. clone ( ) ) . into ( ) )
235270 . await
236- . ok_or_else ( || anyhow:: anyhow!( "Error fetching public key for issuer {raw_issuer}" ) ) ?;
271+ . ok_or_else ( || {
272+ TokenValidationError :: IdentityProviderUnavailable ( anyhow:: anyhow!(
273+ "Error fetching public key for issuer {raw_issuer}"
274+ ) )
275+ } ) ?;
237276 validator. validate_token ( token) . await
238277 }
239278}
@@ -300,7 +339,7 @@ impl TokenValidator for JwksValidator {
300339 log:: debug!( "No key id in header. Trying all keys." ) ;
301340 // TODO: Consider returning an error if no kid is given?
302341 // For now, lets just try all the keys.
303- let mut last_error = TokenValidationError :: Other ( anyhow:: anyhow!( "No kid found" ) ) ;
342+ let mut last_error = TokenValidationError :: InvalidClaims ( anyhow:: anyhow!( "No kid found" ) ) ;
304343 for ( kid, key) in & self . keyset . keys {
305344 log:: debug!( "Trying key {kid}" ) ;
306345 let validator = BasicTokenValidator {
@@ -328,14 +367,31 @@ mod tests {
328367 use crate :: auth:: identity:: { IncomingClaims , SpacetimeIdentityClaims } ;
329368 use crate :: auth:: token_validation:: {
330369 BasicTokenValidator , CachingOidcTokenValidator , FullTokenValidator , OidcTokenValidator , TokenSigner ,
331- TokenValidator ,
370+ TokenValidationError , TokenValidationErrorCategory , TokenValidator ,
332371 } ;
333372 use crate :: auth:: JwtKeys ;
334373 use base64:: Engine ;
335374 use openssl:: ec:: { EcGroup , EcKey } ;
336375 use serde_json;
337376 use spacetimedb_lib:: Identity ;
338377
378+ #[ test]
379+ fn token_validation_error_categories_distinguish_authentication_failures ( ) {
380+ assert_eq ! (
381+ TokenValidationError :: KeyIDNotFound . category( ) ,
382+ TokenValidationErrorCategory :: InvalidCredentials
383+ ) ;
384+ assert_eq ! (
385+ TokenValidationError :: IdentityProviderUnavailable ( anyhow:: anyhow!( "controlled provider failure" ) )
386+ . category( ) ,
387+ TokenValidationErrorCategory :: IdentityProvider
388+ ) ;
389+ assert_eq ! (
390+ TokenValidationError :: Other ( anyhow:: anyhow!( "controlled internal failure" ) ) . category( ) ,
391+ TokenValidationErrorCategory :: Internal
392+ ) ;
393+ }
394+
339395 #[ tokio:: test]
340396 async fn test_local_validator_checks_issuer ( ) -> anyhow:: Result < ( ) > {
341397 // Test that the issuer must match the expected issuer for LocalTokenValidator.
0 commit comments