+29
-10
who-am-i/src/jwt.rs
+29
-10
who-am-i/src/jwt.rs
···
1
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
2
use serde::Serialize;
3
use std::fs;
4
use std::io::Error as IOError;
···
27
28
pub struct Tokens {
29
encoding_key: EncodingKey,
30
-
jwks: String,
31
}
32
33
impl Tokens {
34
-
pub fn from_files(
35
-
priv_f: impl AsRef<Path>,
36
-
jwks_f: impl AsRef<Path>,
37
-
) -> Result<Self, TokensSetupError> {
38
let private_key_data: Vec<u8> =
39
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
40
let encoding_key =
41
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
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)?;
45
46
-
Ok(Self { encoding_key, jwks })
47
}
48
49
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
···
62
)?)
63
}
64
65
-
pub fn jwks(&self) -> String {
66
-
self.jwks.clone()
67
}
68
}
69
···
1
+
use elliptic_curve::SecretKey;
2
+
use jose_jwk::{Class, Jwk, Key, Parameters};
3
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, errors::Error as JWTError};
4
+
use pkcs8::DecodePrivateKey;
5
use serde::Serialize;
6
use std::fs;
7
use std::io::Error as IOError;
···
30
31
pub struct Tokens {
32
encoding_key: EncodingKey,
33
+
jwk: Jwk,
34
}
35
36
impl Tokens {
37
+
pub fn from_files(priv_f: impl AsRef<Path>) -> Result<Self, TokensSetupError> {
38
let private_key_data: Vec<u8> =
39
fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?;
40
let encoding_key =
41
EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?;
42
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
64
65
+
Ok(Self { encoding_key, jwk })
66
}
67
68
pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> {
···
81
)?)
82
}
83
84
+
pub fn jwk(&self) -> Jwk {
85
+
self.jwk.clone()
86
}
87
}
88
+1
-16
who-am-i/src/main.rs
+1
-16
who-am-i/src/main.rs
···
31
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
32
#[arg(long)]
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
/// this server's client-reachable base url, for oauth redirect + jwt check
50
///
51
/// required unless running in localhost mode with --dev
···
100
println!(" - {host}");
101
}
102
103
-
let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap();
104
105
if let Err(e) = install_metrics_server() {
106
eprintln!("failed to install metrics server: {e:?}");
···
31
/// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem
32
#[arg(long)]
33
jwt_private_key: PathBuf,
34
/// this server's client-reachable base url, for oauth redirect + jwt check
35
///
36
/// required unless running in localhost mode with --dev
···
85
println!(" - {host}");
86
}
87
88
+
let tokens = Tokens::from_files(args.jwt_private_key).unwrap();
89
90
if let Err(e) = install_metrics_server() {
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
.route("/auth", get(start_oauth))
100
.route("/authorized", get(complete_oauth))
101
.route("/disconnect", post(disconnect))
102
-
.route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?)
103
.route("/.well-known/jwks.json", get(jwks))
104
.with_state(state);
105
···
315
Json(oauth.client_metadata())
316
}
317
318
-
async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> {
319
-
Json(oauth.jwks())
320
-
}
321
-
322
#[derive(Debug, Deserialize)]
323
struct BeginOauthParams {
324
handle: String,
···
450
(jar, Json(json!({ "ok": true })))
451
}
452
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())
459
}
···
99
.route("/auth", get(start_oauth))
100
.route("/authorized", get(complete_oauth))
101
.route("/disconnect", post(disconnect))
102
.route("/.well-known/jwks.json", get(jwks))
103
.with_state(state);
104
···
314
Json(oauth.client_metadata())
315
}
316
317
#[derive(Debug, Deserialize)]
318
struct BeginOauthParams {
319
handle: String,
···
445
(jar, Json(json!({ "ok": true })))
446
}
447
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)
452
}