Repository: svix/svix-webhooks (language: rust)

Issue:
Show a better error message when auth token == JWT secret
It's been a common point of confusion for people, they miss the section in the docs about authentication and use the jwt secret as the auth token, and then have things fail not knowing why, e.g. https://svixcommunity.slack.com/archives/C022BJ2EBHV/p1692185434398029

It's probably worth it to have a distinct error case for when jwt secret == auth token, or actually maybe we should just always improve the error message to point to the docs (just need to make sure to also update it to the current dashboard link on the hosted server).
Something like:
> Authentication failed. Please refer to https://github.com/svix/svix-webhooks#authentication for more information.


Discussion / hints:

Fix patch:
diff --git a/server/svix-server/src/core/security.rs b/server/svix-server/src/core/security.rs
index 18b316182..c6efb8ac5 100644
--- a/server/svix-server/src/core/security.rs
+++ b/server/svix-server/src/core/security.rs
@@ -72,12 +72,25 @@ pub struct CustomClaim {
     pub feature_flags: FeatureFlagSet,
 }
 
+pub const INVALID_TOKEN_ERR: &str = "Invalid token";
+pub const JWT_SECRET_ERR : &str = "Authentication failed. JWT signing secrets can not be used as tokens, please refer to https://github.com/svix/svix-webhooks#authentication for more information.";
+
 pub async fn permissions_from_bearer(parts: &mut Parts, state: &AppState) -> Result<Permissions> {
     let TypedHeader(Authorization(bearer)) =
         ctx!(TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state).await)?;
 
-    let claims = parse_bearer(&state.cfg.jwt_signing_config, &bearer)
-        .ok_or_else(|| HttpError::unauthorized(None, Some("Invalid token".to_string())))?;
+    let claims =
+        parse_bearer(&state.cfg.jwt_signing_config, &bearer).ok_or_else(|| {
+            match state.cfg.jwt_signing_config.as_ref() {
+                JwtSigningConfig::Default { jwt_secret }
+                    if jwt_secret.to_bytes() == bearer.token().as_bytes() =>
+                {
+                    HttpError::unauthorized(None, Some(JWT_SECRET_ERR.to_string()))
+                }
+
+                _ => HttpError::unauthorized(None, Some(INVALID_TOKEN_ERR.to_string())),
+            }
+        })?;
     let perms = permissions_from_jwt(claims)?;
 
     tracing::Span::current().record("org_id", perms.org_id().to_string());


Test patch:
diff --git a/server/svix-server/tests/e2e_auth.rs b/server/svix-server/tests/e2e_auth.rs
index 2a8b02e2a..68be200d5 100644
--- a/server/svix-server/tests/e2e_auth.rs
+++ b/server/svix-server/tests/e2e_auth.rs
@@ -4,7 +4,14 @@
 
 use reqwest::StatusCode;
 
-use svix_server::{core::types::ApplicationId, v1::endpoints::application::ApplicationOut};
+use serde_json::Value;
+use svix_server::{
+    core::{
+        security::{INVALID_TOKEN_ERR, JWT_SECRET_ERR},
+        types::ApplicationId,
+    },
+    v1::endpoints::application::ApplicationOut,
+};
 
 mod utils;
 use utils::{
@@ -12,6 +19,8 @@ use utils::{
     start_svix_server, IgnoredResponse,
 };
 
+use crate::utils::get_default_test_config;
+
 #[tokio::test]
 /// Users with application-level tokens should only be allowed to read the information related to
 /// their one application. All other endpoints should error.
@@ -100,3 +109,51 @@ async fn test_dashboard_access_without_body() {
         .await
         .unwrap();
 }
+
+#[tokio::test]
+async fn test_invalid_auth_error_detail() {
+    let (mut client, _jh) = start_svix_server().await;
+    let cfg = get_default_test_config();
+    let jwt_secret = match cfg.jwt_signing_config.as_ref() {
+        svix_server::core::security::JwtSigningConfig::Default { jwt_secret } => {
+            std::str::from_utf8(&jwt_secret.to_bytes())
+                .unwrap()
+                .to_owned()
+        }
+
+        _ => return {},
+    };
+
+    client.set_auth_header("some-nonsense-key".to_string());
+    match client
+        .post::<_, Value>(
+            "api/v1/app/",
+            application_in("TEST_APP_NAME"),
+            StatusCode::UNAUTHORIZED,
+        )
+        .await
+    {
+        Ok(Value::Object(i)) => {
+            assert_eq!(i.get("detail").unwrap(), INVALID_TOKEN_ERR);
+        }
+        _ => {
+            panic!("Unexpected response");
+        }
+    }
+    client.set_auth_header(jwt_secret);
+    match client
+        .post::<_, Value>(
+            "api/v1/app/",
+            application_in("TEST_APP_NAME"),
+            StatusCode::UNAUTHORIZED,
+        )
+        .await
+    {
+        Ok(Value::Object(i)) => {
+            assert_eq!(i.get("detail").unwrap(), JWT_SECRET_ERR);
+        }
+        _ => {
+            panic!("Unexpected response");
+        }
+    }
+}