Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

serve jwks for token validation

Changed files
+58 -17
who-am-i
+2 -1
who-am-i/.gitignore
··· 1 - jwt-key.pem 1 + *.pem 2 + jwks.json
+28 -9
who-am-i/src/jwt.rs
··· 3 3 use std::fs; 4 4 use std::io::Error as IOError; 5 5 use std::path::Path; 6 + use std::string::FromUtf8Error; 6 7 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 7 8 use thiserror::Error; 8 9 9 10 #[derive(Debug, Error)] 10 11 pub enum TokensSetupError { 11 - #[error(transparent)] 12 - Io(#[from] IOError), 13 - #[error("failed to retrieve ec key: {0}")] 14 - FromEc(JWTError), 12 + #[error("failed to read private key")] 13 + ReadPrivateKey(IOError), 14 + #[error("failed to retrieve private key: {0}")] 15 + PrivateKey(JWTError), 16 + #[error("failed to read private key")] 17 + ReadJwks(IOError), 18 + #[error("failed to retrieve jwks: {0}")] 19 + DecodeJwks(FromUtf8Error), 15 20 } 16 21 17 22 #[derive(Debug, Error)] 18 23 pub enum TokenMintingError { 19 24 #[error("failed to mint: {0}")] 20 - FromEc(#[from] JWTError), 25 + EncodingError(#[from] JWTError), 21 26 } 22 27 23 28 pub struct Tokens { 24 29 encoding_key: EncodingKey, 30 + jwks: String, 25 31 } 26 32 27 33 impl Tokens { 28 - pub fn from_file(f: impl AsRef<Path>) -> Result<Self, TokensSetupError> { 29 - let data: Vec<u8> = fs::read(f)?; 30 - let encoding_key = EncodingKey::from_ec_pem(&data).map_err(TokensSetupError::FromEc)?; 31 - Ok(Self { encoding_key }) 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 }) 32 47 } 33 48 34 49 pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> { ··· 45 60 &Claims { sub, exp }, 46 61 &self.encoding_key, 47 62 )?) 63 + } 64 + 65 + pub fn jwks(&self) -> String { 66 + self.jwks.clone() 48 67 } 49 68 } 50 69
+19 -7
who-am-i/src/main.rs
··· 15 15 /// eg: `cat /dev/urandom | head -c 64 | base64` 16 16 #[arg(long, env)] 17 17 app_secret: String, 18 - /// path to jwt key (PEM format) 18 + /// path to jwt private key (PEM pk8 format) 19 19 /// 20 20 /// generate with: 21 - /// ```bash 22 - /// openssl ecparam -genkey -noout -name prime256v1 \ 23 - /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem 24 - /// ``` 21 + /// 22 + /// openssl ecparam -genkey -noout -name prime256v1 \ 23 + /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem 25 24 #[arg(long)] 26 - jwt_key: PathBuf, 25 + jwt_private_key: PathBuf, 26 + /// path to pubkeys file (jwks format) 27 + /// 28 + /// get pem of pubkey from private key with: 29 + /// 30 + /// openssl ec -in <PATH-TO-PRIV-KEY>.pem -pubout 31 + /// 32 + /// then convert to a jwk, probably with something less sketchy than an [online tool](https://jwkset.com/generate) 33 + /// 34 + /// wrap the jwk in an array, then in an object under "keys": 35 + /// 36 + /// { "keys": [<JWK obj>] } 37 + #[arg(long)] 38 + jwks: PathBuf, 27 39 /// Enable dev mode 28 40 /// 29 41 /// enables automatic template reloading ··· 54 66 println!(" - {host}"); 55 67 } 56 68 57 - let tokens = Tokens::from_file(args.jwt_key).unwrap(); 69 + let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap(); 58 70 59 71 if let Err(e) = install_metrics_server() { 60 72 eprintln!("failed to install metrics server: {e:?}");
+9
who-am-i/src/server.rs
··· 91 91 .route("/auth", get(start_oauth)) 92 92 .route("/authorized", get(complete_oauth)) 93 93 .route("/disconnect", post(disconnect)) 94 + .route("/.well-known/jwks.json", get(jwks)) 94 95 .with_state(state); 95 96 96 97 let listener = TcpListener::bind("0.0.0.0:9997") ··· 437 438 let jar = jar.remove(DID_COOKIE_KEY); 438 439 (jar, Json(json!({ "ok": true }))) 439 440 } 441 + 442 + async fn jwks(State(AppState { tokens, .. }): State<AppState>) -> impl IntoResponse { 443 + let headers = [ 444 + (CONTENT_TYPE, "application/json"), 445 + // (CACHE_CONTROL, "") // TODO 446 + ]; 447 + (headers, tokens.jwks()) 448 + }