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

Configure Feed

Select the types of activity you want to include in your feed.

dev and prod with all the oauth joy

phil c99e3c33 8304c4de

+254 -24
+113
Cargo.lock
··· 1162 1162 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 1163 1163 dependencies = [ 1164 1164 "const-oid", 1165 + "pem-rfc7468", 1165 1166 "zeroize", 1166 1167 ] 1167 1168 ··· 1344 1345 "elliptic-curve", 1345 1346 "rfc6979", 1346 1347 "signature", 1348 + "spki", 1347 1349 ] 1348 1350 1349 1351 [[package]] ··· 1364 1366 "ff", 1365 1367 "generic-array", 1366 1368 "group", 1369 + "pem-rfc7468", 1370 + "pkcs8", 1367 1371 "rand_core 0.6.4", 1368 1372 "sec1", 1369 1373 "subtle", ··· 2442 2446 "jose-b64", 2443 2447 "jose-jwa", 2444 2448 "p256", 2449 + "p384", 2450 + "rsa", 2445 2451 "serde", 2446 2452 "zeroize", 2447 2453 ] ··· 2485 2491 version = "1.5.0" 2486 2492 source = "registry+https://github.com/rust-lang/crates.io-index" 2487 2493 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2494 + dependencies = [ 2495 + "spin", 2496 + ] 2488 2497 2489 2498 [[package]] 2490 2499 name = "lazycell" ··· 2982 2991 ] 2983 2992 2984 2993 [[package]] 2994 + name = "num-bigint-dig" 2995 + version = "0.8.4" 2996 + source = "registry+https://github.com/rust-lang/crates.io-index" 2997 + checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 2998 + dependencies = [ 2999 + "byteorder", 3000 + "lazy_static", 3001 + "libm", 3002 + "num-integer", 3003 + "num-iter", 3004 + "num-traits", 3005 + "rand 0.8.5", 3006 + "smallvec", 3007 + "zeroize", 3008 + ] 3009 + 3010 + [[package]] 2985 3011 name = "num-conv" 2986 3012 version = "0.1.0" 2987 3013 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3007 3033 ] 3008 3034 3009 3035 [[package]] 3036 + name = "num-iter" 3037 + version = "0.1.45" 3038 + source = "registry+https://github.com/rust-lang/crates.io-index" 3039 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 3040 + dependencies = [ 3041 + "autocfg", 3042 + "num-integer", 3043 + "num-traits", 3044 + ] 3045 + 3046 + [[package]] 3010 3047 name = "num-modular" 3011 3048 version = "0.6.1" 3012 3049 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3028 3065 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 3029 3066 dependencies = [ 3030 3067 "autocfg", 3068 + "libm", 3031 3069 ] 3032 3070 3033 3071 [[package]] ··· 3142 3180 ] 3143 3181 3144 3182 [[package]] 3183 + name = "p384" 3184 + version = "0.13.1" 3185 + source = "registry+https://github.com/rust-lang/crates.io-index" 3186 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 3187 + dependencies = [ 3188 + "elliptic-curve", 3189 + "primeorder", 3190 + ] 3191 + 3192 + [[package]] 3145 3193 name = "parking" 3146 3194 version = "2.2.1" 3147 3195 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3202 3250 dependencies = [ 3203 3251 "base64 0.22.1", 3204 3252 "serde", 3253 + ] 3254 + 3255 + [[package]] 3256 + name = "pem-rfc7468" 3257 + version = "0.7.0" 3258 + source = "registry+https://github.com/rust-lang/crates.io-index" 3259 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 3260 + dependencies = [ 3261 + "base64ct", 3205 3262 ] 3206 3263 3207 3264 [[package]] ··· 3267 3324 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 3268 3325 3269 3326 [[package]] 3327 + name = "pkcs1" 3328 + version = "0.7.5" 3329 + source = "registry+https://github.com/rust-lang/crates.io-index" 3330 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 3331 + dependencies = [ 3332 + "der", 3333 + "pkcs8", 3334 + "spki", 3335 + ] 3336 + 3337 + [[package]] 3338 + name = "pkcs8" 3339 + version = "0.10.2" 3340 + source = "registry+https://github.com/rust-lang/crates.io-index" 3341 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 3342 + dependencies = [ 3343 + "der", 3344 + "spki", 3345 + ] 3346 + 3347 + [[package]] 3270 3348 name = "pkg-config" 3271 3349 version = "0.3.32" 3272 3350 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3666 3744 ] 3667 3745 3668 3746 [[package]] 3747 + name = "rsa" 3748 + version = "0.9.8" 3749 + source = "registry+https://github.com/rust-lang/crates.io-index" 3750 + checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 3751 + dependencies = [ 3752 + "const-oid", 3753 + "digest", 3754 + "num-bigint-dig", 3755 + "num-integer", 3756 + "num-traits", 3757 + "pkcs1", 3758 + "pkcs8", 3759 + "rand_core 0.6.4", 3760 + "signature", 3761 + "spki", 3762 + "subtle", 3763 + "zeroize", 3764 + ] 3765 + 3766 + [[package]] 3669 3767 name = "rustc-demangle" 3670 3768 version = "0.1.24" 3671 3769 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3873 3971 "base16ct", 3874 3972 "der", 3875 3973 "generic-array", 3974 + "pkcs8", 3876 3975 "subtle", 3877 3976 "zeroize", 3878 3977 ] ··· 4266 4365 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 4267 4366 dependencies = [ 4268 4367 "lock_api", 4368 + ] 4369 + 4370 + [[package]] 4371 + name = "spki" 4372 + version = "0.7.3" 4373 + source = "registry+https://github.com/rust-lang/crates.io-index" 4374 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 4375 + dependencies = [ 4376 + "base64ct", 4377 + "der", 4269 4378 ] 4270 4379 4271 4380 [[package]] ··· 5143 5252 "clap", 5144 5253 "ctrlc", 5145 5254 "dashmap", 5255 + "elliptic-curve", 5146 5256 "handlebars", 5147 5257 "hickory-resolver", 5258 + "jose-jwk", 5148 5259 "jsonwebtoken", 5149 5260 "metrics", 5150 5261 "metrics-exporter-prometheus 0.17.2", 5262 + "p256", 5263 + "pkcs8", 5151 5264 "rand 0.9.1", 5152 5265 "reqwest", 5153 5266 "serde",
+4
who-am-i/Cargo.toml
··· 14 14 clap = { version = "4.5.40", features = ["derive", "env"] } 15 15 ctrlc = "3.4.7" 16 16 dashmap = "6.1.0" 17 + elliptic-curve = "0.13.8" 17 18 handlebars = { version = "6.3.2", features = ["dir_source"] } 18 19 hickory-resolver = "0.25.2" 20 + jose-jwk = "0.1.2" 19 21 jsonwebtoken = "9.3.1" 20 22 metrics = "0.24.2" 23 + p256 = "0.13.2" 24 + pkcs8 = "0.10.2" 21 25 rand = "0.9.1" 22 26 reqwest = { version = "0.12.22", features = ["native-tls-vendored"] } 23 27 serde = { version = "1.0.219", features = ["derive"] }
+38 -1
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 at-oauth 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, env)] 25 + oauth_private_key: Option<PathBuf>, 18 26 /// path to jwt private key (PEM pk8 format) 19 27 /// 20 28 /// generate with: ··· 34 42 /// wrap the jwk in an array, then in an object under "keys": 35 43 /// 36 44 /// { "keys": [<JWK obj>] } 45 + /// 46 + /// TODO: remove this, serve automatically 37 47 #[arg(long)] 38 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 52 + #[arg(long, env)] 53 + base_url: Option<String>, 54 + /// host:port to bind to on startup 55 + #[arg(long, env, default_value = "127.0.0.1:9997")] 56 + bind: String, 39 57 /// Enable dev mode 40 58 /// 41 - /// enables automatic template reloading 59 + /// enables automatic template reloading, uses localhost oauth config, etc 42 60 #[arg(long, action)] 43 61 dev: bool, 44 62 /// Hosts who are allowed to one-click auth ··· 57 75 58 76 let args = Args::parse(); 59 77 78 + // let bind = args.bind.to_socket_addrs().expect("--bind must be ToSocketAddrs"); 79 + 80 + let base = args.base_url.unwrap_or_else(|| { 81 + if args.dev { 82 + format!("http://{}", args.bind) 83 + } else { 84 + panic!("not in --dev mode so --base-url is required") 85 + } 86 + }); 87 + 88 + if !args.dev && args.oauth_private_key.is_none() { 89 + panic!("--at-oauth-key is required except in --dev"); 90 + } else if args.dev && args.oauth_private_key.is_some() { 91 + eprintln!("warn: --at-oauth-key is ignored in dev (localhost config)"); 92 + } 93 + 60 94 if args.allowed_hosts.is_empty() { 61 95 panic!("at least one --allowed-host host must be set"); 62 96 } ··· 75 109 serve( 76 110 shutdown, 77 111 args.app_secret, 112 + args.oauth_private_key, 78 113 tokens, 114 + base, 115 + args.bind, 79 116 args.allowed_hosts, 80 117 args.dev, 81 118 )
+77 -21
who-am-i/src/oauth.rs
··· 1 + use jose_jwk::Class; 2 + use jose_jwk::Jwk; 3 + use jose_jwk::Key; 4 + use jose_jwk::Parameters; 5 + use std::fs; 6 + use std::path::PathBuf; 7 + // use p256::SecretKey; 1 8 use atrium_api::{agent::SessionManager, types::string::Did}; 2 9 use atrium_common::resolver::Resolver; 3 10 use atrium_identity::{ ··· 5 12 handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver}, 6 13 }; 7 14 use atrium_oauth::{ 8 - AtprotoLocalhostClientMetadata, AuthorizeOptions, CallbackParams, DefaultHttpClient, 9 - KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope, 15 + AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, AuthorizeOptions, 16 + CallbackParams, DefaultHttpClient, GrantType, KnownScope, OAuthClient, OAuthClientConfig, 17 + OAuthClientMetadata, OAuthResolverConfig, Scope, 10 18 store::{session::MemorySessionStore, state::MemoryStateStore}, 11 19 }; 20 + use elliptic_curve::SecretKey; 12 21 use hickory_resolver::{ResolveError, TokioResolver}; 22 + use jose_jwk::JwkSet; 23 + use pkcs8::DecodePrivateKey; 13 24 use serde::Deserialize; 14 25 use std::sync::Arc; 15 26 use thiserror::Error; ··· 83 94 } 84 95 85 96 impl OAuth { 86 - pub fn new() -> Result<Self, AuthSetupError> { 97 + pub fn new(oauth_private_key: Option<PathBuf>, base: String) -> Result<Self, AuthSetupError> { 87 98 let http_client = Arc::new(DefaultHttpClient::default()); 88 99 let did_resolver = || { 89 100 CommonDidResolver::new(CommonDidResolverConfig { ··· 93 104 }; 94 105 let dns_txt_resolver = 95 106 HickoryDnsTxtResolver::new().map_err(AuthSetupError::HickoryResolverError)?; 96 - let client_config = OAuthClientConfig { 97 - client_metadata: AtprotoLocalhostClientMetadata { 98 - redirect_uris: Some(vec![String::from("http://127.0.0.1:9997/authorized")]), 99 - scopes: Some(READONLY_SCOPE.to_vec()), 100 - }, 101 - keys: None, 102 - resolver: OAuthResolverConfig { 103 - did_resolver: did_resolver(), 104 - handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 105 - dns_txt_resolver, 106 - http_client: Arc::clone(&http_client), 107 - }), 108 - authorization_server_metadata: Default::default(), 109 - protected_resource_metadata: Default::default(), 110 - }, 111 - state_store: MemoryStateStore::default(), 112 - session_store: MemorySessionStore::default(), 107 + 108 + let resolver = OAuthResolverConfig { 109 + did_resolver: did_resolver(), 110 + handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 111 + dns_txt_resolver, 112 + http_client: Arc::clone(&http_client), 113 + }), 114 + authorization_server_metadata: Default::default(), 115 + protected_resource_metadata: Default::default(), 113 116 }; 114 117 115 - let client = OAuthClient::new(client_config).map_err(AuthSetupError::AtriumClientError)?; 118 + let state_store = MemoryStateStore::default(); 119 + let session_store = MemorySessionStore::default(); 120 + 121 + let client = if let Some(path) = oauth_private_key { 122 + let key_contents: Vec<u8> = fs::read(path).unwrap(); 123 + let key_string = String::from_utf8(key_contents).unwrap(); 124 + let key = SecretKey::<p256::NistP256>::from_pkcs8_pem(&key_string) 125 + .map(|secret_key| Jwk { 126 + key: Key::from(&secret_key.into()), 127 + prm: Parameters { 128 + kid: Some("at-oauth-00".to_string()), 129 + cls: Some(Class::Signing), 130 + ..Default::default() 131 + }, 132 + }) 133 + .expect("to get private key"); 134 + OAuthClient::new(OAuthClientConfig { 135 + client_metadata: AtprotoClientMetadata { 136 + client_id: format!("{base}/client-metadata.json"), 137 + client_uri: Some(base.clone()), 138 + redirect_uris: vec![format!("{base}/authorized")], 139 + token_endpoint_auth_method: AuthMethod::PrivateKeyJwt, 140 + grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], 141 + scopes: READONLY_SCOPE.to_vec(), 142 + jwks_uri: Some(format!("{base}/.well-known/at-jwks.json")), 143 + token_endpoint_auth_signing_alg: Some(String::from("ES256")), 144 + }, 145 + keys: Some(vec![key]), 146 + resolver, 147 + state_store, 148 + session_store, 149 + }) 150 + .map_err(AuthSetupError::AtriumClientError)? 151 + } else { 152 + OAuthClient::new(OAuthClientConfig { 153 + client_metadata: AtprotoLocalhostClientMetadata { 154 + redirect_uris: Some(vec![String::from("http://127.0.0.1:9997/authorized")]), 155 + scopes: Some(READONLY_SCOPE.to_vec()), 156 + }, 157 + keys: None, 158 + resolver, 159 + state_store, 160 + session_store, 161 + }) 162 + .map_err(AuthSetupError::AtriumClientError)? 163 + }; 116 164 117 165 Ok(Self { 118 166 client: Arc::new(client), 119 167 did_resolver: Arc::new(did_resolver()), 120 168 }) 169 + } 170 + 171 + pub fn client_metadata(&self) -> OAuthClientMetadata { 172 + self.client.client_metadata.clone() 173 + } 174 + 175 + pub fn jwks(&self) -> JwkSet { 176 + self.client.jwks() 121 177 } 122 178 123 179 pub async fn begin(&self, handle: &str) -> Result<String, atrium_oauth::Error> {
+22 -2
who-am-i/src/server.rs
··· 1 1 use atrium_api::types::string::Did; 2 + use atrium_oauth::OAuthClientMetadata; 2 3 use axum::{ 3 4 Router, 4 5 extract::{FromRef, Json as ExtractJson, Query, State}, ··· 12 13 use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; 13 14 use axum_template::{RenderHtml, engine::Engine}; 14 15 use handlebars::{Handlebars, handlebars_helper}; 16 + use jose_jwk::JwkSet; 17 + use std::path::PathBuf; 15 18 16 19 use serde::Deserialize; 17 20 use serde_json::{Value, json}; ··· 52 55 } 53 56 } 54 57 58 + #[allow(clippy::too_many_arguments)] 55 59 pub async fn serve( 56 60 shutdown: CancellationToken, 57 61 app_secret: String, 62 + oauth_private_key: Option<PathBuf>, 58 63 tokens: Tokens, 64 + base: String, 65 + bind: String, 59 66 allowed_hosts: Vec<String>, 60 67 dev: bool, 61 68 ) { ··· 70 77 // clients have to pick up their identity-resolving tasks within this period 71 78 let task_pickup_expiration = Duration::from_secs(15); 72 79 73 - let oauth = OAuth::new().unwrap(); 80 + let oauth = OAuth::new(oauth_private_key, base).unwrap(); 74 81 75 82 let state = AppState { 76 83 engine: Engine::new(hbs), ··· 88 95 .route("/style.css", get(css)) 89 96 .route("/prompt", get(prompt)) 90 97 .route("/user-info", post(user_info)) 98 + .route("/client-metadata.json", get(client_metadata)) 91 99 .route("/auth", get(start_oauth)) 92 100 .route("/authorized", get(complete_oauth)) 93 101 .route("/disconnect", post(disconnect)) 102 + .route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?) 94 103 .route("/.well-known/jwks.json", get(jwks)) 95 104 .with_state(state); 96 105 97 - let listener = TcpListener::bind("0.0.0.0:9997") 106 + eprintln!("starting server at http://{bind}"); 107 + let listener = TcpListener::bind(bind) 98 108 .await 99 109 .expect("listener binding to work"); 100 110 ··· 297 307 Json(json!({ "handle": handle })).into_response() 298 308 } 299 309 } 310 + } 311 + 312 + async fn client_metadata( 313 + State(AppState { oauth, .. }): State<AppState>, 314 + ) -> Json<OAuthClientMetadata> { 315 + Json(oauth.client_metadata()) 316 + } 317 + 318 + async fn at_jwks(State(AppState { oauth, .. }): State<AppState>) -> Json<JwkSet> { 319 + Json(oauth.jwks()) 300 320 } 301 321 302 322 #[derive(Debug, Deserialize)]