Skip to content

Commit 9a25d3c

Browse files
committed
fix(auth): harden token parsing, improve error handling and config redaction
- Fix Bearer token parsing to require the space delimiter ("Bearer " instead of "Bearer"), preventing tokens with a "Bearer" prefix from being silently accepted with a mangled value - Rename BearerToken::value() to as_str(), returning &str to avoid unnecessary cloning - Fix double-negative in doc comments: "JWT is not invalid" → "JWT is invalid" - Distinguish RowNotFound from other database errors in get_token_generation (mysql + sqlite) instead of mapping all errors to UserNotFound - Propagate database errors properly in verify_token_handler - Only redact auth config fields when present (Some), avoiding overwriting None values; also redact public key fields - Add tracing::warn when configured key paths don't exist and the system falls back to ephemeral keys - Replace hardcoded JWT tokens in doc examples with <JWT_TOKEN> placeholder - Update e2e test environment redaction to match new config logic
1 parent 49504f6 commit 9a25d3c

14 files changed

Lines changed: 59 additions & 34 deletions

File tree

src/config/v2/auth.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::path::Path;
22

33
use serde::{Deserialize, Serialize};
4+
use tracing::warn;
45

56
/// Default session-token lifetime: 2 weeks (1 209 600 s).
67
const DEFAULT_SESSION_TOKEN_LIFETIME_SECS: u64 = 1_209_600;
@@ -123,6 +124,7 @@ impl Auth {
123124
if Path::new(path).exists() {
124125
return Some(std::fs::read(path).unwrap_or_else(|e| panic!("Failed to read RSA private key from `{path}`: {e}")));
125126
}
127+
warn!("configured private_key_path `{path}` does not exist, falling back to ephemeral keys");
126128
}
127129

128130
None
@@ -151,6 +153,7 @@ impl Auth {
151153
if Path::new(path).exists() {
152154
return Some(std::fs::read(path).unwrap_or_else(|e| panic!("Failed to read RSA public key from `{path}`: {e}")));
153155
}
156+
warn!("configured public_key_path `{path}` does not exist, falling back to ephemeral keys");
154157
}
155158

156159
None

src/config/v2/mod.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,18 @@ impl Settings {
111111
let _ = self.database.connect_url.set_password(Some("***"));
112112
}
113113
"***".clone_into(&mut self.mail.smtp.credentials.password);
114-
self.auth.private_key_pem = Some("***-redacted-private-key-pem***".to_owned());
115-
self.auth.private_key_path = Some("***-redacted***".to_owned());
114+
if let Some(private_key_pem) = self.auth.private_key_pem.as_mut() {
115+
"***-redacted-private-key-pem***".clone_into(private_key_pem);
116+
}
117+
if let Some(private_key_path) = self.auth.private_key_path.as_mut() {
118+
"***-redacted***".clone_into(private_key_path);
119+
}
120+
if let Some(public_key_pem) = self.auth.public_key_pem.as_mut() {
121+
"***-redacted-public-key-pem***".clone_into(public_key_pem);
122+
}
123+
if let Some(public_key_path) = self.auth.public_key_path.as_mut() {
124+
"***-redacted***".clone_into(public_key_path);
125+
}
116126
}
117127

118128
/// Encodes the configuration to TOML.

src/databases/mysql.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,11 @@ impl Database for Mysql {
304304
.bind(user_id)
305305
.fetch_one(&self.pool)
306306
.await
307-
.map(|(v,): (i64,)| u64::try_from(v).unwrap_or(0))
308-
.map_err(|_| database::Error::UserNotFound)
307+
.map_err(|e| match e {
308+
sqlx::Error::RowNotFound => database::Error::UserNotFound,
309+
_ => database::Error::Error,
310+
})
311+
.and_then(|(v,): (i64,)| u64::try_from(v).map_err(|_| database::Error::Error))
309312
}
310313

311314
async fn increment_token_generation(&self, user_id: i64) -> Result<(), database::Error> {

src/databases/sqlite.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,11 @@ impl Database for Sqlite {
300300
.bind(user_id)
301301
.fetch_one(&self.pool)
302302
.await
303-
.map(|(v,): (i64,)| u64::try_from(v).unwrap_or(0))
304-
.map_err(|_| database::Error::UserNotFound)
303+
.map_err(|e| match e {
304+
sqlx::Error::RowNotFound => database::Error::UserNotFound,
305+
_ => database::Error::Error,
306+
})
307+
.and_then(|(v,): (i64,)| u64::try_from(v).map_err(|_| database::Error::Error))
305308
}
306309

307310
async fn increment_token_generation(&self, user_id: i64) -> Result<(), database::Error> {

src/web/api/server/v1/auth.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ impl Authentication {
155155
/// This function will return an error if the JWT is invalid, expired,
156156
/// or if the token's generation has been revoked.
157157
pub async fn get_user_id_from_bearer_token(&self, token: BearerToken) -> Result<UserId, AuthError> {
158-
let claims = self.json_web_token.verify(&token.value())?;
158+
let claims = self.json_web_token.verify(token.as_str())?;
159159
self.validate_token_generation(&claims).await?;
160160
Ok(claims.sub)
161161
}
@@ -184,7 +184,7 @@ impl Authentication {
184184
pub fn parse_token(authorization: &HeaderValue) -> Result<String, AuthError> {
185185
let header_str = authorization.to_str().map_err(|_| AuthError::TokenInvalid)?;
186186

187-
let token = header_str.strip_prefix("Bearer").ok_or(AuthError::TokenInvalid)?.trim();
187+
let token = header_str.strip_prefix("Bearer ").ok_or(AuthError::TokenInvalid)?.trim();
188188

189189
if token.is_empty() {
190190
return Err(AuthError::TokenInvalid);

src/web/api/server/v1/contexts/category/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
//! ```bash
8282
//! curl \
8383
//! --header "Content-Type: application/json" \
84-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
84+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
8585
//! --request POST \
8686
//! --data '{"name":"new category","icon":null}' \
8787
//! http://127.0.0.1:3001/v1/category
@@ -119,7 +119,7 @@
119119
//! ```bash
120120
//! curl \
121121
//! --header "Content-Type: application/json" \
122-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
122+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
123123
//! --request DELETE \
124124
//! --data '{"name":"new category","icon":null}' \
125125
//! http://127.0.0.1:3001/v1/category

src/web/api/server/v1/contexts/proxy/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
//!
4848
//! ```bash
4949
//! curl \
50-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
50+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
5151
//! --header "cache-control: no-cache" \
5252
//! --header "pragma: no-cache" \
5353
//! --output mandelbrotset.jpg \

src/web/api/server/v1/contexts/settings/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
//! ```bash
2020
//! curl \
2121
//! --header "Content-Type: application/json" \
22-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
22+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
2323
//! --request GET \
2424
//! "http://127.0.0.1:3001/v1/settings"
2525
//! ```

src/web/api/server/v1/contexts/tag/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
//! ```bash
6262
//! curl \
6363
//! --header "Content-Type: application/json" \
64-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
64+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
6565
//! --request POST \
6666
//! --data '{"name":"new tag"}' \
6767
//! http://127.0.0.1:3001/v1/tag
@@ -98,7 +98,7 @@
9898
//! ```bash
9999
//! curl \
100100
//! --header "Content-Type: application/json" \
101-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
101+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
102102
//! --request DELETE \
103103
//! --data '{"tag_id":1}' \
104104
//! http://127.0.0.1:3001/v1/tag

src/web/api/server/v1/contexts/torrent/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
//! ```bash
4949
//! curl \
5050
//! --header "Content-Type: multipart/form-data" \
51-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
51+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
5252
//! --request POST \
5353
//! --form "title=MandelbrotSet" \
5454
//! --form "description=MandelbrotSet image" \
@@ -87,7 +87,7 @@
8787
//! ```bash
8888
//! curl \
8989
//! --header "Content-Type: application/x-bittorrent" \
90-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
90+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
9191
//! --output mandelbrot_2048x2048_infohash_v1.png.torrent \
9292
//! "http://127.0.0.1:3001/v1/torrent/download/5452869BE36F9F3350CCEE6B4544E7E76CAAADAB"
9393
//! ```
@@ -129,7 +129,7 @@
129129
//! ```bash
130130
//! curl \
131131
//! --header "Content-Type: application/json" \
132-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
132+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
133133
//! --request GET \
134134
//! "http://127.0.0.1:3001/v1/torrent/5452869BE36F9F3350CCEE6B4544E7E76CAAADAB"
135135
//! ```
@@ -215,7 +215,7 @@
215215
//! ```bash
216216
//! curl \
217217
//! --header "Content-Type: application/json" \
218-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
218+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
219219
//! --request GET \
220220
//! "http://127.0.0.1:3001/v1/torrents"
221221
//! ```
@@ -279,7 +279,7 @@
279279
//! ```bash
280280
//! curl \
281281
//! --header "Content-Type: application/json" \
282-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
282+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
283283
//! --request PUT \
284284
//! --data '{"title":"MandelbrotSet", "description":"MandelbrotSet image"}' \
285285
//! "http://127.0.0.1:3001/v1/torrent/5452869BE36F9F3350CCEE6B4544E7E76CAAADAB"
@@ -336,7 +336,7 @@
336336
//! ```bash
337337
//! curl \
338338
//! --header "Content-Type: application/json" \
339-
//! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6InRvcnJ1c3QtaW5kZXgiLCJhdWQiOiJzZXNzaW9uIiwiaWF0IjoxNjg2MjE1Nzg4LCJleHAiOjE2ODc0MjUzODgsInJvbGUiOiJhZG1pbiIsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiJ9.-EfY9CrZz2OLfjiVQzkhxSjV7tWTFivP2yMuzZkbEak" \
339+
//! --header "Authorization: Bearer <JWT_TOKEN>" \
340340
//! --request DELETE \
341341
//! "http://127.0.0.1:3001/v1/torrent/5452869BE36F9F3350CCEE6B4544E7E76CAAADAB"
342342
//! ```

0 commit comments

Comments
 (0)