| 14 |
14 |
|
/// Issuer claim value for all SyncKit JWTs.
|
| 15 |
15 |
|
const SYNCKIT_JWT_ISSUER: &str = "makenotwork-synckit";
|
| 16 |
16 |
|
|
|
17 |
+ |
/// Audience claim value for all SyncKit JWTs. Pinning `aud` (in addition to
|
|
18 |
+ |
/// `iss`) means a token signed with this secret for any other purpose can never
|
|
19 |
+ |
/// be replayed against the sync API, even if the secret were ever shared.
|
|
20 |
+ |
const SYNCKIT_JWT_AUDIENCE: &str = "makenotwork-synckit-clients";
|
|
21 |
+ |
|
| 17 |
22 |
|
/// JWT claims for SyncKit tokens.
|
| 18 |
23 |
|
#[derive(Debug, Serialize, Deserialize)]
|
| 19 |
24 |
|
pub struct SyncClaims {
|
| 27 |
32 |
|
pub key: String,
|
| 28 |
33 |
|
/// Issuer
|
| 29 |
34 |
|
pub iss: String,
|
|
35 |
+ |
/// Audience
|
|
36 |
+ |
pub aud: String,
|
| 30 |
37 |
|
/// Expiration (Unix timestamp)
|
| 31 |
38 |
|
pub exp: i64,
|
| 32 |
39 |
|
/// Issued at (Unix timestamp)
|
| 46 |
53 |
|
app: app_id,
|
| 47 |
54 |
|
key: key.to_string(),
|
| 48 |
55 |
|
iss: SYNCKIT_JWT_ISSUER.to_string(),
|
|
56 |
+ |
aud: SYNCKIT_JWT_AUDIENCE.to_string(),
|
| 49 |
57 |
|
exp: now + SYNCKIT_JWT_EXPIRY_SECS,
|
| 50 |
58 |
|
iat: now,
|
| 51 |
59 |
|
};
|
| 72 |
80 |
|
pub fn decode_sync_token(secret: &str, token: &str) -> Result<SyncClaims, AppError> {
|
| 73 |
81 |
|
let mut validation = Validation::new(Algorithm::HS256);
|
| 74 |
82 |
|
validation.set_issuer(&[SYNCKIT_JWT_ISSUER]);
|
|
83 |
+ |
validation.set_audience(&[SYNCKIT_JWT_AUDIENCE]);
|
| 75 |
84 |
|
|
| 76 |
85 |
|
let data = decode::<SyncClaims>(
|
| 77 |
86 |
|
token,
|
| 195 |
204 |
|
app: app_id,
|
| 196 |
205 |
|
key: TEST_KEY.to_string(),
|
| 197 |
206 |
|
iss: SYNCKIT_JWT_ISSUER.to_string(),
|
|
207 |
+ |
aud: SYNCKIT_JWT_AUDIENCE.to_string(),
|
| 198 |
208 |
|
exp: now - 3600, // expired 1 hour ago
|
| 199 |
209 |
|
iat: now - 7200,
|
| 200 |
210 |
|
};
|
| 251 |
261 |
|
app: app_id,
|
| 252 |
262 |
|
key: TEST_KEY.to_string(),
|
| 253 |
263 |
|
iss: "wrong-issuer".to_string(),
|
|
264 |
+ |
aud: SYNCKIT_JWT_AUDIENCE.to_string(),
|
| 254 |
265 |
|
exp: now + SYNCKIT_JWT_EXPIRY_SECS,
|
| 255 |
266 |
|
iat: now,
|
| 256 |
267 |
|
};
|
| 265 |
276 |
|
assert!(decode_sync_token(TEST_SECRET, &token).is_err());
|
| 266 |
277 |
|
}
|
| 267 |
278 |
|
|
|
279 |
+ |
#[test]
|
|
280 |
+ |
fn wrong_audience_rejected() {
|
|
281 |
+ |
// A token correctly signed and issued but minted for a different
|
|
282 |
+ |
// audience must not authenticate against the sync API.
|
|
283 |
+ |
let now = chrono::Utc::now().timestamp();
|
|
284 |
+ |
let claims = SyncClaims {
|
|
285 |
+ |
sub: UserId::new(),
|
|
286 |
+ |
app: SyncAppId::new(),
|
|
287 |
+ |
key: TEST_KEY.to_string(),
|
|
288 |
+ |
iss: SYNCKIT_JWT_ISSUER.to_string(),
|
|
289 |
+ |
aud: "some-other-audience".to_string(),
|
|
290 |
+ |
exp: now + SYNCKIT_JWT_EXPIRY_SECS,
|
|
291 |
+ |
iat: now,
|
|
292 |
+ |
};
|
|
293 |
+ |
let token = encode(
|
|
294 |
+ |
&Header::default(),
|
|
295 |
+ |
&claims,
|
|
296 |
+ |
&EncodingKey::from_secret(TEST_SECRET.as_bytes()),
|
|
297 |
+ |
)
|
|
298 |
+ |
.unwrap();
|
|
299 |
+ |
assert!(decode_sync_token(TEST_SECRET, &token).is_err());
|
|
300 |
+ |
}
|
|
301 |
+ |
|
| 268 |
302 |
|
#[test]
|
| 269 |
303 |
|
fn missing_claims_rejected() {
|
| 270 |
304 |
|
use serde::Serialize;
|
| 373 |
407 |
|
app: app_id,
|
| 374 |
408 |
|
key: TEST_KEY.to_string(),
|
| 375 |
409 |
|
iss: SYNCKIT_JWT_ISSUER.to_string(),
|
|
410 |
+ |
aud: SYNCKIT_JWT_AUDIENCE.to_string(),
|
| 376 |
411 |
|
exp: now + SYNCKIT_JWT_EXPIRY_SECS,
|
| 377 |
412 |
|
iat: now + 86400 * 365, // 1 year in the future
|
| 378 |
413 |
|
};
|
| 401 |
436 |
|
app: app_id,
|
| 402 |
437 |
|
key: TEST_KEY.to_string(),
|
| 403 |
438 |
|
iss: SYNCKIT_JWT_ISSUER.to_string(),
|
|
439 |
+ |
aud: SYNCKIT_JWT_AUDIENCE.to_string(),
|
| 404 |
440 |
|
exp: now + SYNCKIT_JWT_EXPIRY_SECS,
|
| 405 |
441 |
|
iat: now + 30, // within the 60s skew window
|
| 406 |
442 |
|
};
|