itsi-scheduler 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6950d858938b1a548b2f493c61ef786b93c6067e786d1b3c20490aa89f81b7f1
|
4
|
+
data.tar.gz: ccbc30811c509b4a2ea01b298c1151a982144015d98699aa04826a88cba84aed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81c6a5bf2fc56fc7b1fe911f2e77896f4528f272025447c2819b300400f6d14f9f7d38be9445ec5bb17eb0e2fc1705a9b5d1153009bd357a686b61fade401e45
|
7
|
+
data.tar.gz: e4f2a8ca14ddece21b20f3eee79903f639b596e263b8f1bd9daa792b9236afabbb87391542f5f98cd3a790752b65f8da719d4434f56c869ad4944668e88eafc0
|
data/ext/itsi_server/Cargo.toml
CHANGED
@@ -42,7 +42,7 @@ hyper-util = { version = "0.1.10", features = ["full"] }
|
|
42
42
|
itsi_error = { path = "../itsi_error" }
|
43
43
|
itsi_rb_helpers = { path = "../itsi_rb_helpers" }
|
44
44
|
itsi_tracing = { path = "../itsi_tracing" }
|
45
|
-
|
45
|
+
jsonwebtoken = "9.3.1"
|
46
46
|
magnus = { version = "0.7.1", features = ["bytes", "rb-sys"] }
|
47
47
|
moka = { version = "0.12.10", features = ["sync"] }
|
48
48
|
notify = { version = "8.0.0" }
|
@@ -5,20 +5,14 @@ use crate::server::{
|
|
5
5
|
};
|
6
6
|
use async_trait::async_trait;
|
7
7
|
use base64::{engine::general_purpose, Engine};
|
8
|
+
use derive_more::Debug;
|
8
9
|
use either::Either;
|
9
10
|
use itsi_error::ItsiError;
|
10
|
-
use
|
11
|
-
|
12
|
-
prelude::{
|
13
|
-
ECDSAP256PublicKeyLike, ECDSAP384PublicKeyLike, ES256PublicKey, ES384PublicKey, HS256Key,
|
14
|
-
HS384Key, HS512Key, MACLike, PS256PublicKey, PS384PublicKey, PS512PublicKey,
|
15
|
-
RS256PublicKey, RS384PublicKey, RS512PublicKey, RSAPublicKeyLike,
|
16
|
-
},
|
17
|
-
token::Token,
|
11
|
+
use jsonwebtoken::{
|
12
|
+
decode, decode_header, Algorithm as JwtAlg, DecodingKey, TokenData, Validation,
|
18
13
|
};
|
19
14
|
use magnus::error::Result;
|
20
15
|
use serde::Deserialize;
|
21
|
-
use std::str;
|
22
16
|
use std::{
|
23
17
|
collections::{HashMap, HashSet},
|
24
18
|
sync::OnceLock,
|
@@ -27,9 +21,12 @@ use std::{
|
|
27
21
|
#[derive(Debug, Clone, Deserialize)]
|
28
22
|
pub struct AuthJwt {
|
29
23
|
pub token_source: TokenSource,
|
24
|
+
// The verifiers map still holds base64-encoded key strings keyed by algorithm.
|
30
25
|
pub verifiers: HashMap<JwtAlgorithm, Vec<String>>,
|
26
|
+
// We now store jsonwebtoken’s DecodingKey in our OnceLock.
|
31
27
|
#[serde(skip_deserializing)]
|
32
|
-
|
28
|
+
#[debug(skip)]
|
29
|
+
pub keys: OnceLock<HashMap<JwtAlgorithm, Vec<DecodingKey>>>,
|
33
30
|
pub audiences: Option<HashSet<String>>,
|
34
31
|
pub subjects: Option<HashSet<String>>,
|
35
32
|
pub issuers: Option<HashSet<String>>,
|
@@ -63,161 +60,90 @@ pub enum JwtAlgorithm {
|
|
63
60
|
Ps512,
|
64
61
|
}
|
65
62
|
|
63
|
+
// Allow conversion from jsonwebtoken’s Algorithm to our JwtAlgorithm.
|
64
|
+
impl From<JwtAlg> for JwtAlgorithm {
|
65
|
+
fn from(alg: JwtAlg) -> Self {
|
66
|
+
match alg {
|
67
|
+
JwtAlg::HS256 => JwtAlgorithm::Hs256,
|
68
|
+
JwtAlg::HS384 => JwtAlgorithm::Hs384,
|
69
|
+
JwtAlg::HS512 => JwtAlgorithm::Hs512,
|
70
|
+
JwtAlg::RS256 => JwtAlgorithm::Rs256,
|
71
|
+
JwtAlg::RS384 => JwtAlgorithm::Rs384,
|
72
|
+
JwtAlg::RS512 => JwtAlgorithm::Rs512,
|
73
|
+
JwtAlg::ES256 => JwtAlgorithm::Es256,
|
74
|
+
JwtAlg::ES384 => JwtAlgorithm::Es384,
|
75
|
+
JwtAlg::PS256 => JwtAlgorithm::Ps256,
|
76
|
+
JwtAlg::PS384 => JwtAlgorithm::Ps384,
|
77
|
+
JwtAlg::PS512 => JwtAlgorithm::Ps512,
|
78
|
+
_ => panic!("Unsupported algorithm"),
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
66
83
|
impl JwtAlgorithm {
|
67
|
-
|
84
|
+
/// Given a base64-encoded key string, decode and construct a jsonwebtoken::DecodingKey.
|
85
|
+
pub fn key_from(&self, base64: &str) -> itsi_error::Result<DecodingKey> {
|
68
86
|
let bytes = general_purpose::STANDARD
|
69
87
|
.decode(base64)
|
70
88
|
.map_err(ItsiError::default)?;
|
71
|
-
|
72
89
|
match self {
|
73
|
-
|
74
|
-
JwtAlgorithm::
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
.map(JwtKey::Rs384)
|
91
|
-
.map_err(ItsiError::default)?),
|
92
|
-
JwtAlgorithm::Rs512 => Ok(RS512PublicKey::from_der(&bytes)
|
93
|
-
.or_else(|_| {
|
94
|
-
RS512PublicKey::from_pem(
|
95
|
-
&String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
|
96
|
-
)
|
97
|
-
})
|
98
|
-
.map(JwtKey::Rs512)
|
99
|
-
.map_err(ItsiError::default)?),
|
100
|
-
JwtAlgorithm::Es256 => Ok(ES256PublicKey::from_der(&bytes)
|
101
|
-
.or_else(|_| {
|
102
|
-
ES256PublicKey::from_pem(
|
103
|
-
&String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
|
104
|
-
)
|
105
|
-
})
|
106
|
-
.map(JwtKey::Es256)
|
107
|
-
.map_err(ItsiError::default)?),
|
108
|
-
JwtAlgorithm::Es384 => Ok(ES384PublicKey::from_der(&bytes)
|
109
|
-
.or_else(|_| {
|
110
|
-
ES384PublicKey::from_pem(
|
111
|
-
&String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
|
112
|
-
)
|
113
|
-
})
|
114
|
-
.map(JwtKey::Es384)
|
115
|
-
.map_err(ItsiError::default)?),
|
116
|
-
JwtAlgorithm::Ps256 => Ok(PS256PublicKey::from_der(&bytes)
|
117
|
-
.or_else(|_| {
|
118
|
-
PS256PublicKey::from_pem(
|
119
|
-
&String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
|
120
|
-
)
|
121
|
-
})
|
122
|
-
.map(JwtKey::Ps256)
|
123
|
-
.map_err(ItsiError::default)?),
|
124
|
-
JwtAlgorithm::Ps384 => Ok(PS384PublicKey::from_der(&bytes)
|
125
|
-
.or_else(|_| {
|
126
|
-
PS384PublicKey::from_pem(
|
127
|
-
&String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
|
128
|
-
)
|
129
|
-
})
|
130
|
-
.map(JwtKey::Ps384)
|
131
|
-
.map_err(ItsiError::default)?),
|
132
|
-
JwtAlgorithm::Ps512 => Ok(PS512PublicKey::from_der(&bytes)
|
133
|
-
.or_else(|_| {
|
134
|
-
PS512PublicKey::from_pem(
|
135
|
-
&String::from_utf8(bytes.clone()).map_err(ItsiError::default)?,
|
136
|
-
)
|
137
|
-
})
|
138
|
-
.map(JwtKey::Ps512)
|
139
|
-
.map_err(ItsiError::default)?),
|
90
|
+
// For HMAC algorithms, use the secret directly.
|
91
|
+
JwtAlgorithm::Hs256 | JwtAlgorithm::Hs384 | JwtAlgorithm::Hs512 => {
|
92
|
+
Ok(DecodingKey::from_secret(&bytes))
|
93
|
+
}
|
94
|
+
// For RSA (and PS) algorithms, expect a PEM-formatted key.
|
95
|
+
JwtAlgorithm::Rs256
|
96
|
+
| JwtAlgorithm::Rs384
|
97
|
+
| JwtAlgorithm::Rs512
|
98
|
+
| JwtAlgorithm::Ps256
|
99
|
+
| JwtAlgorithm::Ps384
|
100
|
+
| JwtAlgorithm::Ps512 => {
|
101
|
+
DecodingKey::from_rsa_pem(&bytes).map_err(|e| ItsiError::default(e.to_string()))
|
102
|
+
}
|
103
|
+
// For ECDSA algorithms, expect a PEM-formatted key.
|
104
|
+
JwtAlgorithm::Es256 | JwtAlgorithm::Es384 => {
|
105
|
+
DecodingKey::from_ec_pem(&bytes).map_err(|e| ItsiError::default(e.to_string()))
|
106
|
+
}
|
140
107
|
}
|
141
108
|
}
|
142
109
|
}
|
143
110
|
|
144
|
-
#[derive(Debug,
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
Rs256(RS256PublicKey),
|
150
|
-
Rs384(RS384PublicKey),
|
151
|
-
Rs512(RS512PublicKey),
|
152
|
-
Es256(ES256PublicKey),
|
153
|
-
Es384(ES384PublicKey),
|
154
|
-
Ps256(PS256PublicKey),
|
155
|
-
Ps384(PS384PublicKey),
|
156
|
-
Ps512(PS512PublicKey),
|
111
|
+
#[derive(Debug, Deserialize)]
|
112
|
+
#[serde(untagged)]
|
113
|
+
enum Audience {
|
114
|
+
Single(String),
|
115
|
+
Multiple(Vec<String>),
|
157
116
|
}
|
158
117
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
"rs384" => Ok(JwtAlgorithm::Rs384),
|
169
|
-
"rs512" => Ok(JwtAlgorithm::Rs512),
|
170
|
-
"es256" => Ok(JwtAlgorithm::Es256),
|
171
|
-
"es384" => Ok(JwtAlgorithm::Es384),
|
172
|
-
"ps256" => Ok(JwtAlgorithm::Ps256),
|
173
|
-
"ps384" => Ok(JwtAlgorithm::Ps384),
|
174
|
-
"ps512" => Ok(JwtAlgorithm::Ps512),
|
175
|
-
_ => Err(itsi_error::ItsiError::UnsupportedProtocol(
|
176
|
-
"Unsupported JWT Algorithm".to_string(),
|
177
|
-
)),
|
178
|
-
}
|
179
|
-
}
|
180
|
-
}
|
181
|
-
|
182
|
-
impl JwtKey {
|
183
|
-
pub fn verify(
|
184
|
-
&self,
|
185
|
-
token: &str,
|
186
|
-
) -> std::result::Result<JWTClaims<claims::NoCustomClaims>, jwt_simple::Error> {
|
187
|
-
match self {
|
188
|
-
JwtKey::Hs256(key) => key.verify_token::<NoCustomClaims>(token, None),
|
189
|
-
JwtKey::Hs384(key) => key.verify_token::<NoCustomClaims>(token, None),
|
190
|
-
JwtKey::Hs512(key) => key.verify_token::<NoCustomClaims>(token, None),
|
191
|
-
JwtKey::Rs256(key) => key.verify_token::<NoCustomClaims>(token, None),
|
192
|
-
JwtKey::Rs384(key) => key.verify_token::<NoCustomClaims>(token, None),
|
193
|
-
JwtKey::Rs512(key) => key.verify_token::<NoCustomClaims>(token, None),
|
194
|
-
JwtKey::Es256(key) => key.verify_token::<NoCustomClaims>(token, None),
|
195
|
-
JwtKey::Es384(key) => key.verify_token::<NoCustomClaims>(token, None),
|
196
|
-
JwtKey::Ps256(key) => key.verify_token::<NoCustomClaims>(token, None),
|
197
|
-
JwtKey::Ps384(key) => key.verify_token::<NoCustomClaims>(token, None),
|
198
|
-
JwtKey::Ps512(key) => key.verify_token::<NoCustomClaims>(token, None),
|
199
|
-
}
|
200
|
-
}
|
118
|
+
#[derive(Debug, Deserialize)]
|
119
|
+
struct Claims {
|
120
|
+
// Here we assume the token includes an expiration.
|
121
|
+
#[allow(dead_code)]
|
122
|
+
exp: usize,
|
123
|
+
// The audience claim may be a single string or an array.
|
124
|
+
aud: Option<Audience>,
|
125
|
+
sub: Option<String>,
|
126
|
+
iss: Option<String>,
|
201
127
|
}
|
202
128
|
|
203
129
|
#[async_trait]
|
204
130
|
impl MiddlewareLayer for AuthJwt {
|
205
131
|
async fn initialize(&self) -> Result<()> {
|
206
|
-
let keys: HashMap<JwtAlgorithm, Vec<
|
132
|
+
let keys: HashMap<JwtAlgorithm, Vec<DecodingKey>> = self
|
207
133
|
.verifiers
|
208
134
|
.iter()
|
209
135
|
.map(|(algorithm, key_strings)| {
|
210
136
|
let algo = algorithm.clone();
|
211
|
-
let keys: Result<Vec<
|
137
|
+
let keys: itsi_error::Result<Vec<DecodingKey>> = key_strings
|
212
138
|
.iter()
|
213
139
|
.map(|key_string| algorithm.key_from(key_string))
|
214
140
|
.collect();
|
215
141
|
keys.map(|keys| (algo, keys))
|
216
142
|
})
|
217
|
-
.collect::<Result<HashMap<JwtAlgorithm, Vec<
|
143
|
+
.collect::<itsi_error::Result<HashMap<JwtAlgorithm, Vec<DecodingKey>>>>()?;
|
218
144
|
self.keys
|
219
145
|
.set(keys)
|
220
|
-
.map_err(|
|
146
|
+
.map_err(|_| ItsiError::default("Failed to set keys".to_string()))?;
|
221
147
|
Ok(())
|
222
148
|
}
|
223
149
|
|
@@ -226,6 +152,7 @@ impl MiddlewareLayer for AuthJwt {
|
|
226
152
|
req: HttpRequest,
|
227
153
|
_context: &mut RequestContext,
|
228
154
|
) -> Result<Either<HttpRequest, HttpResponse>> {
|
155
|
+
// Retrieve the JWT token from either a header or a query parameter.
|
229
156
|
let token_str = match &self.token_source {
|
230
157
|
TokenSource::Header { name, prefix } => {
|
231
158
|
if let Some(header) = req.header(name) {
|
@@ -246,45 +173,61 @@ impl MiddlewareLayer for AuthJwt {
|
|
246
173
|
self.error_response.to_http_response(&req).await,
|
247
174
|
));
|
248
175
|
}
|
249
|
-
|
250
176
|
let token_str = token_str.unwrap();
|
251
|
-
let token_meta = Token::decode_metadata(token_str);
|
252
177
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
}
|
258
|
-
let token_meta: std::result::Result<JwtAlgorithm, ItsiError> =
|
259
|
-
token_meta.unwrap().algorithm().try_into();
|
260
|
-
if token_meta.is_err() {
|
261
|
-
return Ok(Either::Right(
|
262
|
-
self.error_response.to_http_response(&req).await,
|
263
|
-
));
|
264
|
-
}
|
265
|
-
let algorithm = token_meta.unwrap();
|
178
|
+
// Use jsonwebtoken's decode_header to inspect the token and determine its algorithm.
|
179
|
+
let header =
|
180
|
+
decode_header(token_str).map_err(|_| ItsiError::default("Invalid token header"))?;
|
181
|
+
let alg: JwtAlgorithm = header.alg.into();
|
266
182
|
|
267
|
-
if !self.verifiers.contains_key(&
|
183
|
+
if !self.verifiers.contains_key(&alg) {
|
268
184
|
return Ok(Either::Right(
|
269
185
|
self.error_response.to_http_response(&req).await,
|
270
186
|
));
|
271
187
|
}
|
188
|
+
let keys = self.keys.get().unwrap().get(&alg).unwrap();
|
272
189
|
|
273
|
-
|
190
|
+
// Build validation based on the algorithm and optional leeway.
|
191
|
+
let mut validation = Validation::new(match alg {
|
192
|
+
JwtAlgorithm::Hs256 => JwtAlg::HS256,
|
193
|
+
JwtAlgorithm::Hs384 => JwtAlg::HS384,
|
194
|
+
JwtAlgorithm::Hs512 => JwtAlg::HS512,
|
195
|
+
JwtAlgorithm::Rs256 => JwtAlg::RS256,
|
196
|
+
JwtAlgorithm::Rs384 => JwtAlg::RS384,
|
197
|
+
JwtAlgorithm::Rs512 => JwtAlg::RS512,
|
198
|
+
JwtAlgorithm::Es256 => JwtAlg::ES256,
|
199
|
+
JwtAlgorithm::Es384 => JwtAlg::ES384,
|
200
|
+
JwtAlgorithm::Ps256 => JwtAlg::PS256,
|
201
|
+
JwtAlgorithm::Ps384 => JwtAlg::PS384,
|
202
|
+
JwtAlgorithm::Ps512 => JwtAlg::PS512,
|
203
|
+
});
|
204
|
+
if let Some(leeway) = self.leeway {
|
205
|
+
validation.leeway = leeway;
|
206
|
+
}
|
207
|
+
// (Optional) You could set expected issuer or audience on `validation` here.
|
274
208
|
|
275
|
-
|
276
|
-
|
209
|
+
// Try verifying the token using each key until one succeeds.
|
210
|
+
let token_data: Option<TokenData<Claims>> = keys
|
211
|
+
.iter()
|
212
|
+
.find_map(|key| decode::<Claims>(token_str, key, &validation).ok());
|
213
|
+
let token_data = if let Some(data) = token_data {
|
214
|
+
data
|
215
|
+
} else {
|
277
216
|
return Ok(Either::Right(
|
278
217
|
self.error_response.to_http_response(&req).await,
|
279
218
|
));
|
280
|
-
}
|
219
|
+
};
|
281
220
|
|
282
|
-
let claims =
|
221
|
+
let claims = token_data.claims;
|
283
222
|
|
223
|
+
// Verify expected audiences.
|
284
224
|
if let Some(expected_audiences) = &self.audiences {
|
285
|
-
|
286
|
-
|
287
|
-
|
225
|
+
if let Some(aud) = &claims.aud {
|
226
|
+
let token_auds: HashSet<String> = match aud {
|
227
|
+
Audience::Single(s) => [s.clone()].into_iter().collect(),
|
228
|
+
Audience::Multiple(v) => v.iter().cloned().collect(),
|
229
|
+
};
|
230
|
+
if expected_audiences.is_disjoint(&token_auds) {
|
288
231
|
return Ok(Either::Right(
|
289
232
|
self.error_response.to_http_response(&req).await,
|
290
233
|
));
|
@@ -292,10 +235,10 @@ impl MiddlewareLayer for AuthJwt {
|
|
292
235
|
}
|
293
236
|
}
|
294
237
|
|
238
|
+
// Verify expected subject.
|
295
239
|
if let Some(expected_subjects) = &self.subjects {
|
296
|
-
|
297
|
-
|
298
|
-
if !expected_subjects.contains(subject) {
|
240
|
+
if let Some(sub) = &claims.sub {
|
241
|
+
if !expected_subjects.contains(sub) {
|
299
242
|
return Ok(Either::Right(
|
300
243
|
self.error_response.to_http_response(&req).await,
|
301
244
|
));
|
@@ -303,10 +246,10 @@ impl MiddlewareLayer for AuthJwt {
|
|
303
246
|
}
|
304
247
|
}
|
305
248
|
|
249
|
+
// Verify expected issuer.
|
306
250
|
if let Some(expected_issuers) = &self.issuers {
|
307
|
-
|
308
|
-
|
309
|
-
if !expected_issuers.contains(issuer) {
|
251
|
+
if let Some(iss) = &claims.iss {
|
252
|
+
if !expected_issuers.contains(iss) {
|
310
253
|
return Ok(Either::Right(
|
311
254
|
self.error_response.to_http_response(&req).await,
|
312
255
|
));
|