+29
-10
who-am-i/src/jwt.rs
+29
-10
who-am-i/src/jwt.rs
···
1
+
use elliptic_curve::SecretKey;
2
+
use jose_jwk::{Class, Jwk, Key, Parameters};
1
3
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
4
+
use pkcs8::DecodePrivateKey;
2
5
use serde::Serialize;
3
6
use std::fs;
4
7
use std::io::Error as IOError;
···
27
30
28
31
pub struct Tokens {
29
32
encoding_key: EncodingKey,
30
-
jwks: String,
33
+
jwk: Jwk,
31
34
}
32
35
33
36
impl Tokens {
34
-
pub fn from_files(
35
-
priv_f: impl AsRef<Path>,
36
-
jwks_f: impl AsRef<Path>,
37
-
) -> Result<Self, TokensSetupError> {
37
+
pub fn from_files(priv_f: impl AsRef<Path>) -> Result<Self, TokensSetupError> {
38
38
let private_key_data: Vec<u8> =
39
39
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
40
40
let encoding_key =
41
41
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
42
42
43
-
let jwks_data: Vec<u8> = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?;
44
-
let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?;
43
+
let jwk_key_string = String::from_utf8(private_key_data).unwrap();
44
+
let mut jwk = SecretKey::<p256::NistP256>::from_pkcs8_pem(&jwk_key_string)
45
+
.map(|secret_key| Jwk {
46
+
key: Key::from(&secret_key.into()),
47
+
prm: Parameters {
48
+
kid: Some("who-am-i-00".to_string()),
49
+
cls: Some(Class::Signing),
50
+
..Default::default()
51
+
},
52
+
})
53
+
.expect("to get private key");
54
+
55
+
// CRITICAL: this is what turns the private jwk into a public one: the
56
+
// `d` parameter is the secret for an EC key; a pubkey just has no `d`.
57
+
//
58
+
// this feels baaaadd but hey we're just copying atrium
59
+
// https://github.com/atrium-rs/atrium/blob/b48810f84d83d037ee89b79b8566df9e0f2a6dae/atrium-oauth/src/keyset.rs#L41
60
+
let Key::Ec(ref mut ec) = jwk.key else {
61
+
unimplemented!()
62
+
};
63
+
ec.d = None; // CRITICAL
45
64
46
-
Ok(Self { encoding_key, jwks })
65
+
Ok(Self { encoding_key, jwk })
47
66
}
48
67
49
68
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
···
62
81
)?)
63
82
}
64
83
65
-
pub fn jwks(&self) -> String {
66
-
self.jwks.clone()
84
+
pub fn jwk(&self) -> Jwk {
85
+
self.jwk.clone()
67
86
}
68
87
}
69
88
+1
-16
who-am-i/src/main.rs
+1
-16
who-am-i/src/main.rs
···
31
31
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
32
32
#[arg(long)]
33
33
jwt_private_key: PathBuf,
34
-
/// path to pubkeys file (jwks format)
35
-
///
36
-
/// get pem of pubkey from private key with:
37
-
///
38
-
/// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout
39
-
///
40
-
/// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate)
41
-
///
42
-
/// wrap the jwk in an array, then in an object under "keys":
43
-
///
44
-
/// { "keys": [<JWK obj>] }
45
-
///
46
-
/// TODO: remove this, serve automatically
47
-
#[arg(long)]
48
-
jwks: PathBuf,
49
34
/// this server's client-reachable base url, for oauth redirect + jwt check
50
35
///
51
36
/// required unless running in localhost mode with --dev
···
100
85
println!(" - {host}");
101
86
}
102
87
103
-
let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap();
88
+
let tokens = Tokens::from_files(args.jwt_private_key).unwrap();
104
89
105
90
if let Err(e) = install_metrics_server() {
106
91
eprintln!("failed to install metrics server: {e:?}");
+4
-11
who-am-i/src/server.rs
+4
-11
who-am-i/src/server.rs
···
99
99
.route("/auth", get(start_oauth))
100
100
.route("/authorized", get(complete_oauth))
101
101
.route("/disconnect", post(disconnect))
102
-
.route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?)
103
102
.route("/.well-known/jwks.json", get(jwks))
104
103
.with_state(state);
105
104
···
315
314
Json(oauth.client_metadata())
316
315
}
317
316
318
-
async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> {
319
-
Json(oauth.jwks())
320
-
}
321
-
322
317
#[derive(Debug, Deserialize)]
323
318
struct BeginOauthParams {
324
319
handle: String,
···
450
445
(jar, Json(json!({ "ok": true })))
451
446
}
452
447
453
-
async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse {
454
-
let headers = [
455
-
(CONTENT_TYPE, "application/json"),
456
-
// (CACHE_CONTROL, "") // TODO
457
-
];
458
-
(headers, tokens.jwks())
448
+
async fn jwks(State(AppState { oauth, tokens, .. }): State<AppState>) -> Json<JwkSet> {
449
+
let mut jwks = oauth.jwks();
450
+
jwks.keys.push(tokens.jwk());
451
+
Json(jwks)
459
452
}