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

serve jwks for token validation

+58 -17
+2 -1
who-am-i/.gitignore
··· 1 - jwt-key.pem
··· 1 + *.pem 2 + jwks.json
+28 -9
who-am-i/src/jwt.rs
··· 3 use std::fs; 4 use std::io::Error as IOError; 5 use std::path::Path; 6 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 7 use thiserror::Error; 8 9 #[derive(Debug, Error)] 10 pub enum TokensSetupError { 11 - #[error(transparent)] 12 - Io(#[from] IOError), 13 - #[error("failed to retrieve ec key: {0}")] 14 - FromEc(JWTError), 15 } 16 17 #[derive(Debug, Error)] 18 pub enum TokenMintingError { 19 #[error("failed to mint: {0}")] 20 - FromEc(#[from] JWTError), 21 } 22 23 pub struct Tokens { 24 encoding_key: EncodingKey, 25 } 26 27 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 }) 32 } 33 34 pub fn mint(&self, t: impl ToString) -> Result<String, TokenMintingError> { ··· 45 &Claims { sub, exp }, 46 &self.encoding_key, 47 )?) 48 } 49 } 50
··· 3 use std::fs; 4 use std::io::Error as IOError; 5 use std::path::Path; 6 + use std::string::FromUtf8Error; 7 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 8 use thiserror::Error; 9 10 #[derive(Debug, Error)] 11 pub enum TokensSetupError { 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), 20 } 21 22 #[derive(Debug, Error)] 23 pub enum TokenMintingError { 24 #[error("failed to mint: {0}")] 25 + EncodingError(#[from] JWTError), 26 } 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> { ··· 60 &Claims { sub, exp }, 61 &self.encoding_key, 62 )?) 63 + } 64 + 65 + pub fn jwks(&self) -> String { 66 + self.jwks.clone() 67 } 68 } 69
+19 -7
who-am-i/src/main.rs
··· 15 /// eg: `cat /dev/urandom | head -c 64 | base64` 16 #[arg(long, env)] 17 app_secret: String, 18 - /// path to jwt key (PEM format) 19 /// 20 /// generate with: 21 - /// ```bash 22 - /// openssl ecparam -genkey -noout -name prime256v1 \ 23 - /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-JWT-KEY>.pem 24 - /// ``` 25 #[arg(long)] 26 - jwt_key: PathBuf, 27 /// Enable dev mode 28 /// 29 /// enables automatic template reloading ··· 54 println!(" - {host}"); 55 } 56 57 - let tokens = Tokens::from_file(args.jwt_key).unwrap(); 58 59 if let Err(e) = install_metrics_server() { 60 eprintln!("failed to install metrics server: {e:?}");
··· 15 /// eg: `cat /dev/urandom | head -c 64 | base64` 16 #[arg(long, env)] 17 app_secret: String, 18 + /// path to jwt private key (PEM pk8 format) 19 /// 20 /// generate with: 21 + /// 22 + /// openssl ecparam -genkey -noout -name prime256v1 \ 23 + /// | openssl pkcs8 -topk8 -nocrypt -out <PATH-TO-PRIV-KEY>.pem 24 #[arg(long)] 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, 39 /// Enable dev mode 40 /// 41 /// enables automatic template reloading ··· 66 println!(" - {host}"); 67 } 68 69 + let tokens = Tokens::from_files(args.jwt_private_key, args.jwks).unwrap(); 70 71 if let Err(e) = install_metrics_server() { 72 eprintln!("failed to install metrics server: {e:?}");
+9
who-am-i/src/server.rs
··· 91 .route("/auth", get(start_oauth)) 92 .route("/authorized", get(complete_oauth)) 93 .route("/disconnect", post(disconnect)) 94 .with_state(state); 95 96 let listener = TcpListener::bind("0.0.0.0:9997") ··· 437 let jar = jar.remove(DID_COOKIE_KEY); 438 (jar, Json(json!({ "ok": true }))) 439 }
··· 91 .route("/auth", get(start_oauth)) 92 .route("/authorized", get(complete_oauth)) 93 .route("/disconnect", post(disconnect)) 94 + .route("/.well-known/jwks.json", get(jwks)) 95 .with_state(state); 96 97 let listener = TcpListener::bind("0.0.0.0:9997") ··· 438 let jar = jar.remove(DID_COOKIE_KEY); 439 (jar, Json(json!({ "ok": true }))) 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 + }