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

make jwts with the did in em

+59
Cargo.lock
··· 1697 1697 checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 1698 1698 dependencies = [ 1699 1699 "cfg-if", 1700 + "js-sys", 1700 1701 "libc", 1701 1702 "wasi 0.11.0+wasi-snapshot-preview1", 1703 + "wasm-bindgen", 1702 1704 ] 1703 1705 1704 1706 [[package]] ··· 2455 2457 ] 2456 2458 2457 2459 [[package]] 2460 + name = "jsonwebtoken" 2461 + version = "9.3.1" 2462 + source = "registry+https://github.com/rust-lang/crates.io-index" 2463 + checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" 2464 + dependencies = [ 2465 + "base64 0.22.1", 2466 + "js-sys", 2467 + "pem", 2468 + "ring", 2469 + "serde", 2470 + "serde_json", 2471 + "simple_asn1", 2472 + ] 2473 + 2474 + [[package]] 2458 2475 name = "langtag" 2459 2476 version = "0.3.4" 2460 2477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2955 2972 ] 2956 2973 2957 2974 [[package]] 2975 + name = "num-bigint" 2976 + version = "0.4.6" 2977 + source = "registry+https://github.com/rust-lang/crates.io-index" 2978 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 2979 + dependencies = [ 2980 + "num-integer", 2981 + "num-traits", 2982 + ] 2983 + 2984 + [[package]] 2958 2985 name = "num-conv" 2959 2986 version = "0.1.0" 2960 2987 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2971 2998 ] 2972 2999 2973 3000 [[package]] 3001 + name = "num-integer" 3002 + version = "0.1.46" 3003 + source = "registry+https://github.com/rust-lang/crates.io-index" 3004 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 3005 + dependencies = [ 3006 + "num-traits", 3007 + ] 3008 + 3009 + [[package]] 2974 3010 name = "num-modular" 2975 3011 version = "0.6.1" 2976 3012 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3159 3195 ] 3160 3196 3161 3197 [[package]] 3198 + name = "pem" 3199 + version = "3.0.5" 3200 + source = "registry+https://github.com/rust-lang/crates.io-index" 3201 + checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" 3202 + dependencies = [ 3203 + "base64 0.22.1", 3204 + "serde", 3205 + ] 3206 + 3207 + [[package]] 3162 3208 name = "percent-encoding" 3163 3209 version = "2.3.1" 3164 3210 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4084 4130 dependencies = [ 4085 4131 "digest", 4086 4132 "rand_core 0.6.4", 4133 + ] 4134 + 4135 + [[package]] 4136 + name = "simple_asn1" 4137 + version = "0.6.3" 4138 + source = "registry+https://github.com/rust-lang/crates.io-index" 4139 + checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" 4140 + dependencies = [ 4141 + "num-bigint", 4142 + "num-traits", 4143 + "thiserror 2.0.12", 4144 + "time", 4087 4145 ] 4088 4146 4089 4147 [[package]] ··· 5087 5145 "dashmap", 5088 5146 "handlebars", 5089 5147 "hickory-resolver", 5148 + "jsonwebtoken", 5090 5149 "metrics", 5091 5150 "metrics-exporter-prometheus 0.17.2", 5092 5151 "rand 0.9.1",
+1
who-am-i/.gitignore
··· 1 + jwt-key.pem
+1
who-am-i/Cargo.toml
··· 16 16 dashmap = "6.1.0" 17 17 handlebars = { version = "6.3.2", features = ["dir_source"] } 18 18 hickory-resolver = "0.25.2" 19 + jsonwebtoken = "9.3.1" 19 20 metrics = "0.24.2" 20 21 rand = "0.9.1" 21 22 reqwest = { version = "0.12.22", features = ["native-tls-vendored"] }
+2
who-am-i/demo/index.html
··· 12 12 13 13 <body> 14 14 <h1>hey <span id="who"></span></h1> 15 + <p><code id="jwt"></code></p> 15 16 16 17 <iframe src="http://127.0.0.1:9997/prompt" id="whoami" style="border: none" height="160" width="320"></iframe> 17 18 ··· 27 28 window.removeEventListener('message', handleMessage); 28 29 29 30 document.getElementById('who').textContent = ev.data.handle; 31 + document.getElementById('jwt').textContent = ev.data.token; 30 32 } 31 33 window.addEventListener('message', handleMessage); 32 34 })(document.getElementById('whoami'));
+55
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; 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> { 35 + let sub = t.to_string(); 36 + 37 + let dt_now = SystemTime::now() 38 + .duration_since(UNIX_EPOCH) 39 + .expect("unix epoch is in the past"); 40 + let dt_exp = dt_now + Duration::from_secs(30 * 86_400); 41 + let exp = dt_exp.as_secs(); 42 + 43 + Ok(encode( 44 + &Header::new(Algorithm::ES256), 45 + &Claims { sub, exp }, 46 + &self.encoding_key, 47 + )?) 48 + } 49 + } 50 + 51 + #[derive(Debug, Serialize)] 52 + struct Claims { 53 + sub: String, 54 + exp: u64, 55 + }
+2
who-am-i/src/lib.rs
··· 1 1 mod expiring_task_map; 2 + mod jwt; 2 3 mod oauth; 3 4 mod server; 4 5 5 6 pub use expiring_task_map::ExpiringTaskMap; 7 + pub use jwt::Tokens; 6 8 pub use oauth::{OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; 7 9 pub use server::serve;
+22 -3
who-am-i/src/main.rs
··· 1 1 use clap::{ArgAction, Parser}; 2 - use metrics_exporter_prometheus::{PrometheusBuilder, BuildError as PromBuildError}; 2 + use metrics_exporter_prometheus::{BuildError as PromBuildError, PrometheusBuilder}; 3 + use std::path::PathBuf; 3 4 use tokio_util::sync::CancellationToken; 4 - use who_am_i::serve; 5 + use who_am_i::{Tokens, serve}; 5 6 6 7 /// Aggregate links in the at-mosphere 7 8 #[derive(Parser, Debug, Clone)] ··· 14 15 /// eg: `cat /dev/urandom | head -c 64 | base64` 15 16 #[arg(long, env)] 16 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, 17 27 /// Enable dev mode 18 28 /// 19 29 /// enables automatic template reloading ··· 44 54 println!(" - {host}"); 45 55 } 46 56 57 + let tokens = Tokens::from_file(args.jwt_key).unwrap(); 58 + 47 59 if let Err(e) = install_metrics_server() { 48 60 eprintln!("failed to install metrics server: {e:?}"); 49 61 }; 50 62 51 - serve(shutdown, args.app_secret, args.allowed_hosts, args.dev).await; 63 + serve( 64 + shutdown, 65 + args.app_secret, 66 + tokens, 67 + args.allowed_hosts, 68 + args.dev, 69 + ) 70 + .await; 52 71 } 53 72 54 73 fn install_metrics_server() -> Result<(), PromBuildError> {
+30 -1
who-am-i/src/server.rs
··· 22 22 use tokio_util::sync::CancellationToken; 23 23 use url::Url; 24 24 25 - use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; 25 + use crate::{ 26 + ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError, Tokens, 27 + }; 26 28 27 29 const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); 28 30 const STYLE_CSS: &str = include_str!("../static/style.css"); ··· 41 43 pub oauth: Arc<OAuth>, 42 44 pub resolve_handles: ExpiringTaskMap<Result<String, ResolveHandleError>>, 43 45 pub shutdown: CancellationToken, 46 + pub tokens: Arc<Tokens>, 44 47 } 45 48 46 49 impl FromRef<AppState> for Key { ··· 52 55 pub async fn serve( 53 56 shutdown: CancellationToken, 54 57 app_secret: String, 58 + tokens: Tokens, 55 59 allowed_hosts: Vec<String>, 56 60 dev: bool, 57 61 ) { ··· 75 79 oauth: Arc::new(oauth), 76 80 resolve_handles: ExpiringTaskMap::new(task_pickup_expiration), 77 81 shutdown: shutdown.clone(), 82 + tokens: Arc::new(tokens), 78 83 }; 79 84 80 85 let app = Router::new() ··· 166 171 oauth, 167 172 resolve_handles, 168 173 shutdown, 174 + tokens, 169 175 .. 170 176 }): State<AppState>, 171 177 jar: SignedCookieJar, ··· 213 219 214 220 // push cookie expiry 215 221 let jar = jar.add(cookie(&did)); 222 + 223 + let token = match tokens.mint(&*did) { 224 + Ok(t) => t, 225 + Err(e) => { 226 + eprintln!("failed to create JWT: {e:?}"); 227 + return err("failed to create JWT", false); 228 + } 229 + }; 216 230 217 231 let fetch_key = resolve_handles.dispatch( 218 232 { ··· 226 240 metrics::counter!("whoami_auth_prompt", "ok" => "true", "known" => "true").increment(1); 227 241 let info = json!({ 228 242 "did": did, 243 + "token": token, 229 244 "fetch_key": fetch_key, 230 245 "parent_host": parent_host, 231 246 "parent_origin": parent_origin, ··· 340 355 resolve_handles, 341 356 oauth, 342 357 shutdown, 358 + tokens, 343 359 .. 344 360 }): State<AppState>, 345 361 Query(params): Query<OAuthCallbackParams>, ··· 386 402 387 403 let jar = jar.add(cookie(&did)); 388 404 405 + let token = match tokens.mint(&*did) { 406 + Ok(t) => t, 407 + Err(e) => { 408 + eprintln!("failed to create JWT: {e:?}"); 409 + return err( 410 + StatusCode::INTERNAL_SERVER_ERROR, 411 + "fail", 412 + "failed to create JWT", 413 + ); 414 + } 415 + }; 416 + 389 417 let fetch_key = resolve_handles.dispatch( 390 418 { 391 419 let oauth = oauth.clone(); ··· 398 426 metrics::counter!("whoami_auth_complete", "ok" => "true").increment(1); 399 427 let info = json!({ 400 428 "did": did, 429 + "token": token, 401 430 "fetch_key": fetch_key, 402 431 }); 403 432 (jar, RenderHtml("authorized", engine, info)).into_response()
+1
who-am-i/templates/authorized.hbs
··· 8 8 localStorage.setItem("who-am-i", JSON.stringify({ 9 9 result: "success", 10 10 did: {{{json did}}}, 11 + token: {{{json token}}}, 11 12 fetch_key: {{{json fetch_key}}}, 12 13 })); 13 14 window.close();
+4 -4
who-am-i/templates/prompt.hbs
··· 55 55 56 56 loaderEl.classList.add('hidden'); 57 57 handleViewEl.textContent = `@${handle}`; 58 - allowEl.addEventListener('click', () => shareAllow(handle)); 58 + allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}})); 59 59 })(); 60 60 61 61 // anon user ··· 108 108 109 109 const handle = await lookUp(parsed.fetch_key); 110 110 111 - shareAllow(handle); 111 + shareAllow(handle, token); 112 112 }); 113 113 114 114 async function lookUp(fetch_key) { ··· 125 125 return info.handle; 126 126 } 127 127 128 - const shareAllow = handle => { 128 + const shareAllow = (handle, token) => { 129 129 top.postMessage( 130 - { action: "allow", handle }, 130 + { action: "allow", handle, token }, 131 131 {{{json parent_origin}}}, 132 132 ); 133 133 }