Skip to content

Commit bde64bd

Browse files
committed
feat(server): add JWT-based authorization mode
This mode is an alternative to whitelist authorization mode. It extracts the JWT from the authorization header (bearer token), validates token's signature, claimed expiry times and additional (user-configurable) claims.
1 parent 19447aa commit bde64bd

File tree

17 files changed

+565
-153
lines changed

17 files changed

+565
-153
lines changed

crates/notary/client/src/client.rs

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use http_body_util::{BodyExt as _, Either, Empty, Full};
77
use hyper::{
88
body::{Bytes, Incoming},
99
client::conn::http1::Parts,
10+
header::AUTHORIZATION,
1011
Request, Response, StatusCode,
1112
};
1213
use hyper_util::rt::TokioIo;
@@ -136,6 +137,10 @@ pub struct NotaryClient {
136137
/// in notary server.
137138
#[builder(setter(into, strip_option), default)]
138139
api_key: Option<String>,
140+
/// JWT token used to callnotary server endpoints if JWT authorization is enabled
141+
/// in notary server.
142+
#[builder(setter(into, strip_option), default)]
143+
jwt: Option<String>,
139144
/// The duration of notarization request timeout in seconds.
140145
#[builder(default = "60")]
141146
request_timeout: usize,
@@ -290,6 +295,11 @@ impl NotaryClient {
290295
configuration_request_builder.header(X_API_KEY_HEADER, api_key);
291296
}
292297

298+
if let Some(jwt) = &self.jwt {
299+
configuration_request_builder =
300+
configuration_request_builder.header(AUTHORIZATION, format!("Bearer {jwt}"));
301+
}
302+
293303
let configuration_request = configuration_request_builder
294304
.body(Either::Left(Full::new(Bytes::from(
295305
configuration_request_payload,

crates/notary/server/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ config = { version = "0.14", features = ["yaml"] }
3232
csv = { version = "1.3" }
3333
eyre = { version = "0.6" }
3434
futures-util = { workspace = true }
35+
jsonwebtoken = { version = "9.3.1", features = ["use_pem"] }
3536
http = { workspace = true }
3637
http-body-util = { workspace = true }
3738
hyper = { workspace = true, features = ["client", "http1", "server"] }
@@ -45,6 +46,7 @@ pkcs8 = { workspace = true, features = ["pem"] }
4546
rustls = { workspace = true }
4647
rustls-pemfile = { workspace = true }
4748
serde = { workspace = true, features = ["derive"] }
49+
serde_json = { workspace = true }
4850
serde_yaml = { version = "0.9" }
4951
sha1 = { version = "0.10" }
5052
structopt = { version = "0.3" }

crates/notary/server/README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,33 @@ After calling the configuration endpoint above, the prover can proceed to start
145145
Currently, both the private key (and cert) used to establish a TLS connection with the prover, and the private key used by the notary server to sign the notarized transcript, are hardcoded PEM keys stored in this repository. Though the paths of these keys can be changed in the config (`notary-key` field) to use different keys instead.
146146

147147
#### Authorization
148-
An optional authorization module is available to only allow requests with a valid API key attached in the custom HTTP header `X-API-Key`. The API key whitelist path (as well as the flag to enable/disable this module) can be changed in the config (`authorization` field).
148+
An optional authorization module is available to only allow requests with a valid credential attached. Currently, two modes are supported: whitelist and JWT.
149+
150+
Please note that only *one* mode can be active at any one time.
151+
152+
##### Whitelist mode
153+
In whitelist mode, an API key is attached in the custom HTTP header `X-API-Key`. The API key whitelist path (as well as the flag to enable/disable this module) can be changed in the config (`authorization` field).
149154

150155
Hot reloading of the whitelist is supported, i.e. modification of the whitelist file will be automatically applied without needing to restart the server. Please take note of the following
151156
- Avoid using auto save mode when editing the whitelist to prevent spamming hot reloads
152157
- Once the edit is saved, ensure that it has been reloaded successfully by checking the server log
153158

159+
##### JWT mode
160+
In JWT mode, JSON Web Token is attached in the standard `Authorization` HTTP header as a bearer token. The path to decoding key as well as custom user claims can be changed in the
161+
config (`authorization` field).
162+
163+
An example JWT config may look something like this:
164+
165+
```yaml
166+
authorization:
167+
enabled: true
168+
jwt:
169+
public_key_pem_path: "./fixture/auth/jwt.key.pub"
170+
claims:
171+
- name: sub
172+
values: ["tlsnotary"]
173+
```
174+
154175
#### Optional TLS
155176
TLS between the prover and the notary is currently manually handled in this server, though it can be turned off if any of the following is true
156177
- This server is run locally

crates/notary/server/config/config.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ logging:
4343

4444
authorization:
4545
enabled: false
46-
whitelist_csv_path: "./fixture/auth/whitelist.csv"
46+
whitelist: "./fixture/auth/whitelist.csv"
47+
# jwt:
48+
# public_key_pem_path: "./fixture/auth/jwt.key.pub"
49+
# claims:
50+
# - name: sub
51+
# values: ["tlsnotary"]
4752

4853
concurrency: 32
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
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
51+
-----END RSA PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxkfGQo2iyUK6sV84rvsb
3+
6d4IlorFaX4WDwDnEP/zU2Pduwf7kV39x6oqJzNjmfXm/RkcaAZXQdbvjBA9uwM7
4+
cd2Z7hMWLT3oix66Qv9d3+PWcdJtTUVK710+QflZJqOEFOt30eNHm/8pN6z1P6YS
5+
ZvYpHMVlCC7tL8OLWcMH9gYmUH6cWOdzCaCigdQD1CqE9TG7jZlIepiWI7QMD7v/
6+
yN8tZoV8EzW25JdGPe4gtetBl1O+QoAUpuQp7JSUxV4RR8HArGGSQ0OHHZMcfWx/
7+
CoLBUyXlmSCC21sSl634l7+HUM6U/dHo1b+XSMpjjVCTjd0lDFCU+kiLtapdcCiJ
8+
DijSj+a4xkJP+uvxpTZuB6A4W31eDeKPIeutPAwcnw8UktdX6eiAt1ONAxB+ytZN
9+
cdrAUbMdIcocrMxgyPtCWBgDX1LKfpPlRTBRI50mdFkIOp9dh02ijpWAYXaFA65a
10+
I8Tqh9j3wzbvFsCWpBfK7Zcbl7BZskAvXCjnPHDdup5sesnYrhOOG4jo2/nho7Am
11+
EEkvZVDyH9jDPrA6imljFX1gpKvWmtERY7eor3gPI6FFOUh8qwEOB4lTsj9DUd75
12+
vvRlaPU7ibvFTdLVoRiXkAraKR59WmcOpAfut9yOxNaE+M25jY/Jj+crptEt0Mme
13+
zfowRimt3wpQ0C7i6hCBgrMCAwEAAQ==
14+
-----END PUBLIC KEY-----

crates/notary/server/openapi.yaml

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ paths:
1515
security:
1616
- {} # make security optional
1717
- ApiKeyAuth: []
18+
- BearerAuth: []
1819
responses:
1920
'200':
2021
description: Ok response from server
@@ -38,6 +39,7 @@ paths:
3839
security:
3940
- {} # make security optional
4041
- ApiKeyAuth: []
42+
- BearerAuth: []
4143
responses:
4244
'200':
4345
description: Info response from server
@@ -60,6 +62,7 @@ paths:
6062
security:
6163
- {} # make security optional
6264
- ApiKeyAuth: []
65+
- BearerAuth: []
6366
parameters:
6467
- in: header
6568
name: Content-Type
@@ -212,4 +215,9 @@ components:
212215
type: apiKey
213216
in: header
214217
name: X-API-Key
215-
description: Whitelisted API key if auth module is turned on
218+
description: Whitelisted API key if auth module is turned on and in whitelist mode
219+
BearerAuth:
220+
type: http
221+
scheme: bearer
222+
bearerFormat: JWT
223+
description: JSON Web Token if auth module is turned on and in JWT mode

crates/notary/server/src/config.rs

+48-1
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,55 @@ impl Default for NotaryServerProperties {
3636
pub struct AuthorizationProperties {
3737
/// Switch to turn on or off auth middleware
3838
pub enabled: bool,
39+
/// Authorization mode to use: JWT or whitelist
40+
#[serde(flatten)]
41+
pub mode: Option<AuthorizationModeProperties>,
42+
}
43+
44+
#[derive(Clone, Debug, Deserialize)]
45+
#[serde(rename_all = "kebab-case")]
46+
pub enum AuthorizationModeProperties {
47+
/// JWT authorization properties
48+
Jwt(JwtAuthorizationProperties),
3949
/// File path of the whitelist API key csv
40-
pub whitelist_csv_path: Option<String>,
50+
Whitelist(String),
51+
}
52+
53+
impl AuthorizationModeProperties {
54+
pub fn as_whitelist(&self) -> Option<String> {
55+
match self {
56+
Self::Jwt(..) => None,
57+
Self::Whitelist(path) => Some(path.clone()),
58+
}
59+
}
60+
}
61+
62+
#[derive(Clone, Debug, Deserialize, Default)]
63+
pub struct JwtAuthorizationProperties {
64+
/// File path to JWT public key for verifying token signatures
65+
pub public_key_pem_path: String,
66+
/// Set of required JWT claims
67+
#[serde(default)]
68+
pub claims: Vec<JwtClaim>,
69+
}
70+
71+
#[derive(Clone, Debug, Deserialize, Default)]
72+
pub struct JwtClaim {
73+
/// Name of the claim
74+
pub name: String,
75+
/// Optional set of expected values for the claim
76+
#[serde(default)]
77+
pub values: Vec<String>,
78+
/// Optional expected type for the claim
79+
#[serde(default)]
80+
pub value_type: JwtClaimValueType,
81+
}
82+
83+
#[derive(Clone, Debug, Deserialize, Default)]
84+
#[serde(rename_all = "kebab-case")]
85+
pub enum JwtClaimValueType {
86+
#[default]
87+
String,
4188
}
4289

4390
#[derive(Clone, Debug, Deserialize, Default)]

0 commit comments

Comments
 (0)