Skip to content

Commit 88819e0

Browse files
authored
Server: JWT Signing Algorithm Expansion (svix#994)
## Motivation See svix#983. The JWT library that we use exposes a number of different signing algorithms, and it'd be neat to allow people to use some of the more common ones instead of the default HS256. ## Solution This PR adds the `JwtSigningConfig` enum to the configuration which wraps the algorithms exposed by `jwt-simple`. It's a fun, double-layered enum to allow a fallback shape for backwards compatibility purposes. The hard part is the deserialization. After that, the signing config delegates generation and verification calls to whichever kind of key we have loaded. The signing config is wrapped in an `Arc` to avoid having many copies of the key in memory, and also because several of the algorithms are not `Clone`. The non-`Default` variants are wrapped in a `Box` to avoid wasting a ton of space when the `Default` variant is used (which is probably going to be most of the time). Finally, the signing config has a custom Debug impl that just spits out the name of the algorithm in play, to avoid accidentally leaking the key.
1 parent 272d915 commit 88819e0

File tree

12 files changed

+322
-56
lines changed

12 files changed

+322
-56
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ composer.lock
77

88
#IDE
99
.vscode
10+
.idea

server/svix-server/config.default.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ listen_address = "0.0.0.0:8071"
1818
# The JWT secret for authentication - should be secret and securely generated
1919
# jwt_secret = "8KjzRXrKkd9YFcNyqLSIY8JwiaCeRc6WK4UkMnSW"
2020

21+
# This determines the signature algorithm used when creating JWTs
22+
# Supported: HS256 (default), HS384, HS512, RS256, RS384, RS512, EdDSA
23+
# jwt_algorithm = "HS256"
24+
2125
# This determines the type of key that is generated for endpoint secrets by default (when none is set).
2226
# Supported: hmac256 (default), ed25519
2327
# Note: this does not affect existing keys, which will continue signing based on the type they were created with.
@@ -101,4 +105,4 @@ worker_enabled = true
101105
# whitelist_subnets = []
102106

103107
# Maximum number of concurrent worker tasks to spawn (0 is unlimited)
104-
worker_max_tasks = 500
108+
worker_max_tasks = 500

server/svix-server/src/cfg.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
11
// SPDX-FileCopyrightText: © 2022 Svix Authors
22
// SPDX-License-Identifier: MIT
33

4-
use std::{borrow::Cow, collections::HashMap, net::SocketAddr, sync::Arc};
4+
use std::{borrow::Cow, collections::HashMap, net::SocketAddr, sync::Arc, time::Duration};
55

66
use figment::{
77
providers::{Env, Format, Toml},
88
Figment,
99
};
1010
use ipnet::IpNet;
11-
use std::time::Duration;
12-
13-
use crate::{core::cryptography::Encryption, core::security::Keys, error::Result};
1411
use serde::{Deserialize, Deserializer};
1512
use tracing::Level;
1613
use validator::{Validate, ValidationError};
1714

18-
fn deserialize_jwt_secret<'de, D>(deserializer: D) -> std::result::Result<Keys, D::Error>
19-
where
20-
D: Deserializer<'de>,
21-
{
22-
let buf = String::deserialize(deserializer)?;
23-
24-
Ok(Keys::new(buf.as_bytes()))
25-
}
15+
use crate::{core::cryptography::Encryption, core::security::JwtSigningConfig, error::Result};
2616

2717
fn deserialize_main_secret<'de, D>(deserializer: D) -> std::result::Result<Encryption, D::Error>
2818
where
@@ -100,9 +90,9 @@ pub struct ConfigurationInner {
10090
)]
10191
pub encryption: Encryption,
10292

103-
/// The JWT secret for authentication - should be secret and securely generated
104-
#[serde(deserialize_with = "deserialize_jwt_secret")]
105-
pub jwt_secret: Keys,
93+
/// Contains the secret and algorithm for signing JWTs
94+
#[serde(flatten)]
95+
pub jwt_signing_config: Arc<JwtSigningConfig>,
10696

10797
/// This determines the type of key that is generated for endpoint secrets by default (when none is set).
10898
/// Supported: hmac256 (default), ed25519
@@ -380,6 +370,7 @@ pub fn load() -> Result<Arc<ConfigurationInner>> {
380370
#[cfg(test)]
381371
mod tests {
382372
use super::*;
373+
use crate::core::security::JWTAlgorithm;
383374

384375
#[test]
385376
fn test_retry_schedule_parsing() {
@@ -449,4 +440,36 @@ mod tests {
449440
assert_eq!(cfg.queue_backend(), QueueBackend::Redis("test_a"));
450441
assert_eq!(cfg.cache_backend(), CacheBackend::Redis("test_b"));
451442
}
443+
444+
#[test]
445+
fn test_jwt_signing_fallback() {
446+
let raw_config = r#"
447+
jwt_secret = "not_actually_a_secret"
448+
"#;
449+
450+
let actual: JwtSigningConfig = Figment::new()
451+
.merge(Toml::string(raw_config))
452+
.extract()
453+
.unwrap();
454+
455+
assert!(matches!(actual, JwtSigningConfig::Default { .. }));
456+
}
457+
458+
#[test]
459+
fn test_jwt_select_algorithm() {
460+
let raw_config = r#"
461+
jwt_secret = "not_actually_a_secret"
462+
jwt_algorithm = "HS512"
463+
"#;
464+
465+
let actual: JwtSigningConfig = Figment::new()
466+
.merge(Toml::string(raw_config))
467+
.extract()
468+
.unwrap();
469+
470+
assert!(matches!(
471+
actual,
472+
JwtSigningConfig::Advanced(JWTAlgorithm::HS512(_))
473+
));
474+
}
452475
}

server/svix-server/src/core/idempotency.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ mod tests {
434434
// Generate a new token so that keys are unique
435435
dotenv::dotenv().ok();
436436
let cfg = crate::cfg::load().unwrap();
437-
let token = generate_org_token(&cfg.jwt_secret, OrganizationId::new(None, None))
437+
let token = generate_org_token(&cfg.jwt_signing_config, OrganizationId::new(None, None))
438438
.unwrap()
439439
.to_string();
440440

@@ -536,7 +536,7 @@ mod tests {
536536
dotenv::dotenv().ok();
537537
let cfg = crate::cfg::load().unwrap();
538538

539-
let token = generate_org_token(&cfg.jwt_secret, OrganizationId::new(None, None))
539+
let token = generate_org_token(&cfg.jwt_signing_config, OrganizationId::new(None, None))
540540
.unwrap()
541541
.to_string();
542542

@@ -631,7 +631,7 @@ mod tests {
631631
// Generate a new token so that keys are unique
632632
dotenv::dotenv().ok();
633633
let cfg = crate::cfg::load().unwrap();
634-
let token = generate_org_token(&cfg.jwt_secret, OrganizationId::new(None, None))
634+
let token = generate_org_token(&cfg.jwt_signing_config, OrganizationId::new(None, None))
635635
.unwrap()
636636
.to_string();
637637

server/svix-server/src/core/operational_webhooks.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ use serde::Serialize;
1212
use svix::api::{MessageIn, Svix, SvixOptions};
1313

1414
use super::{
15-
security::{generate_management_token, Keys},
15+
security::generate_management_token,
1616
types::{
1717
ApplicationId, ApplicationUid, EndpointId, EndpointUid, MessageAttemptId, MessageId,
1818
MessageUid, OrganizationId,
1919
},
2020
};
21+
use crate::core::security::JwtSigningConfig;
2122
use crate::{
2223
db::models::{endpoint, messageattempt},
23-
error::{HttpError, Result},
24+
error::{Error, HttpError, Result},
25+
location,
2426
};
2527

2628
/// Sent when an endpoint has been automatically disabled after continuous failures.
@@ -107,13 +109,16 @@ pub enum OperationalWebhook {
107109
pub type OperationalWebhookSender = Arc<OperationalWebhookSenderInner>;
108110

109111
pub struct OperationalWebhookSenderInner {
110-
keys: Keys,
112+
signing_config: Arc<JwtSigningConfig>,
111113
url: Option<String>,
112114
}
113115

114116
impl OperationalWebhookSenderInner {
115-
pub fn new(keys: Keys, url: Option<String>) -> Arc<Self> {
116-
Arc::new(Self { keys, url })
117+
pub fn new(keys: Arc<JwtSigningConfig>, url: Option<String>) -> Arc<Self> {
118+
Arc::new(Self {
119+
signing_config: keys,
120+
url,
121+
})
117122
}
118123

119124
pub async fn send_operational_webhook(
@@ -126,8 +131,8 @@ impl OperationalWebhookSenderInner {
126131
None => return Ok(()),
127132
};
128133

129-
let op_webhook_token =
130-
generate_management_token(&self.keys).expect("Error generating Svix Management token");
134+
let op_webhook_token = generate_management_token(&self.signing_config)
135+
.map_err(|e| Error::generic(e, location!()))?;
131136
let svix_api = Svix::new(
132137
op_webhook_token,
133138
Some(SvixOptions {

0 commit comments

Comments
 (0)