diff --git a/who-am-i/.gitignore b/who-am-i/.gitignore --- a/who-am-i/.gitignore +++ b/who-am-i/.gitignore @@ -1,1 +1,2 @@ -jwt-key.pem +*.pem +jwks.json diff --git a/who-am-i/src/jwt.rs b/who-am-i/src/jwt.rs --- a/who-am-i/src/jwt.rs +++ b/who-am-i/src/jwt.rs @@ -3,32 +3,47 @@ use std::fs; use std::io::Error as IOError; use std::path::Path; +use std::string::FromUtf8Error; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use thiserror::Error; #[derive(Debug, Error)] pub enum TokensSetupError { - #[error(transparent)] - Io(#[from] IOError), - #[error("failed to retrieve ec key: {0}")] - FromEc(JWTError), + #[error("failed to read private key")] + ReadPrivateKey(IOError), + #[error("failed to retrieve private key: {0}")] + PrivateKey(JWTError), + #[error("failed to read private key")] + ReadJwks(IOError), + #[error("failed to retrieve jwks: {0}")] + DecodeJwks(FromUtf8Error), } #[derive(Debug, Error)] pub enum TokenMintingError { #[error("failed to mint: {0}")] - FromEc(#[from] JWTError), + EncodingError(#[from] JWTError), } pub struct Tokens { encoding_key: EncodingKey, + jwks: String, } impl Tokens { - pub fn from_file(f: impl AsRef) -> Result { - let data: Vec = fs::read(f)?; - let encoding_key = EncodingKey::from_ec_pem(&data).map_err(TokensSetupError::FromEc)?; - Ok(Self { encoding_key }) + pub fn from_files( + priv_f: impl AsRef, + jwks_f: impl AsRef, + ) -> Result { + let private_key_data: Vec = + fs::read(priv_f).map_err(TokensSetupError::ReadPrivateKey)?; + let encoding_key = + EncodingKey::from_ec_pem(&private_key_data).map_err(TokensSetupError::PrivateKey)?; + + let jwks_data: Vec = fs::read(jwks_f).map_err(TokensSetupError::ReadJwks)?; + let jwks = String::from_utf8(jwks_data).map_err(TokensSetupError::DecodeJwks)?; + + Ok(Self { encoding_key, jwks }) } pub fn mint(&self, t: impl ToString) -> Result { @@ -45,6 +60,10 @@ &Claims { sub, exp }, &self.encoding_key, )?) + } + + pub fn jwks(&self) -> String { + self.jwks.clone() } } diff --git a/who-am-i/src/main.rs b/who-am-i/src/main.rs --- a/who-am-i/src/main.rs +++ b/who-am-i/src/main.rs @@ -15,15 +15,27 @@ /// eg: `cat /dev/urandom | head -c 64 | base64` #[arg(long, env)] app_secret: String, - /// path to jwt key (PEM format) + /// path to jwt private key (PEM pk8 format) /// /// generate with: - /// ```bash - /// openssl ecparam -genkey -noout -name prime256v1 \ - /// | openssl pkcs8 -topk8 -nocrypt -out .pem - /// ``` + /// + /// openssl ecparam -genkey -noout -name prime256v1 \ + /// | openssl pkcs8 -topk8 -nocrypt -out .pem #[arg(long)] - jwt_key: PathBuf, + jwt_private_key: PathBuf, + /// path to pubkeys file (jwks format) + /// + /// get pem of pubkey from private key with: + /// + /// openssl ec -in .pem -pubout + /// + /// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate) + /// + /// wrap the jwk in an array, then in an object under "keys": + /// + /// { "keys": [] } + #[arg(long)] + jwks: PathBuf, /// Enable dev mode /// /// enables automatic template reloading @@ -54,7 +66,7 @@ println!(" - {host}"); } - let tokens = Tokens::from_file(args.jwt_key).unwrap(); + let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap(); if let Err(e) = install_metrics_server() { eprintln!("failed to install metrics server: {e:?}"); diff --git a/who-am-i/src/server.rs b/who-am-i/src/server.rs --- a/who-am-i/src/server.rs +++ b/who-am-i/src/server.rs @@ -91,6 +91,7 @@ .route("/auth", get(start_oauth)) .route("/authorized", get(complete_oauth)) .route("/disconnect", post(disconnect)) + .route("/.well-known/jwks.json", get(jwks)) .with_state(state); let listener = TcpListener::bind("0.0.0.0:9997") @@ -436,4 +437,12 @@ metrics::counter!("whoami_disconnect").increment(1); let jar = jar.remove(DID_COOKIE_KEY); (jar, Json(json!({ "ok": true }))) +} + +async fn jwks(State(AppState { tokens, .. }): State) -> impl IntoResponse { + let headers = [ + (CONTENT_TYPE, "application/json"), + // (CACHE_CONTROL, "") // TODO + ]; + (headers, tokens.jwks()) }