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

dev and prod with all the oauth joy

+254 -24
+113
Cargo.lock
··· 1162 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 1163 dependencies = [ 1164 "const-oid", 1165 "zeroize", 1166 ] 1167 ··· 1344 "elliptic-curve", 1345 "rfc6979", 1346 "signature", 1347 ] 1348 1349 [[package]] ··· 1364 "ff", 1365 "generic-array", 1366 "group", 1367 "rand_core 0.6.4", 1368 "sec1", 1369 "subtle", ··· 2442 "jose-b64", 2443 "jose-jwa", 2444 "p256", 2445 "serde", 2446 "zeroize", 2447 ] ··· 2485 version = "1.5.0" 2486 source = "registry+https://github.com/rust-lang/crates.io-index" 2487 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2488 2489 [[package]] 2490 name = "lazycell" ··· 2982 ] 2983 2984 [[package]] 2985 name = "num-conv" 2986 version = "0.1.0" 2987 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3007 ] 3008 3009 [[package]] 3010 name = "num-modular" 3011 version = "0.6.1" 3012 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3028 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 3029 dependencies = [ 3030 "autocfg", 3031 ] 3032 3033 [[package]] ··· 3142 ] 3143 3144 [[package]] 3145 name = "parking" 3146 version = "2.2.1" 3147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3202 dependencies = [ 3203 "base64 0.22.1", 3204 "serde", 3205 ] 3206 3207 [[package]] ··· 3267 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 3268 3269 [[package]] 3270 name = "pkg-config" 3271 version = "0.3.32" 3272 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3666 ] 3667 3668 [[package]] 3669 name = "rustc-demangle" 3670 version = "0.1.24" 3671 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3873 "base16ct", 3874 "der", 3875 "generic-array", 3876 "subtle", 3877 "zeroize", 3878 ] ··· 4266 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 4267 dependencies = [ 4268 "lock_api", 4269 ] 4270 4271 [[package]] ··· 5143 "clap", 5144 "ctrlc", 5145 "dashmap", 5146 "handlebars", 5147 "hickory-resolver", 5148 "jsonwebtoken", 5149 "metrics", 5150 "metrics-exporter-prometheus 0.17.2", 5151 "rand 0.9.1", 5152 "reqwest", 5153 "serde",
··· 1162 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 1163 dependencies = [ 1164 "const-oid", 1165 + "pem-rfc7468", 1166 "zeroize", 1167 ] 1168 ··· 1345 "elliptic-curve", 1346 "rfc6979", 1347 "signature", 1348 + "spki", 1349 ] 1350 1351 [[package]] ··· 1366 "ff", 1367 "generic-array", 1368 "group", 1369 + "pem-rfc7468", 1370 + "pkcs8", 1371 "rand_core 0.6.4", 1372 "sec1", 1373 "subtle", ··· 2446 "jose-b64", 2447 "jose-jwa", 2448 "p256", 2449 + "p384", 2450 + "rsa", 2451 "serde", 2452 "zeroize", 2453 ] ··· 2491 version = "1.5.0" 2492 source = "registry+https://github.com/rust-lang/crates.io-index" 2493 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2494 + dependencies = [ 2495 + "spin", 2496 + ] 2497 2498 [[package]] 2499 name = "lazycell" ··· 2991 ] 2992 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]] 3011 name = "num-conv" 3012 version = "0.1.0" 3013 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3033 ] 3034 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]] 3047 name = "num-modular" 3048 version = "0.6.1" 3049 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3065 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 3066 dependencies = [ 3067 "autocfg", 3068 + "libm", 3069 ] 3070 3071 [[package]] ··· 3180 ] 3181 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]] 3193 name = "parking" 3194 version = "2.2.1" 3195 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3250 dependencies = [ 3251 "base64 0.22.1", 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", 3262 ] 3263 3264 [[package]] ··· 3324 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 3325 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]] 3348 name = "pkg-config" 3349 version = "0.3.32" 3350 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3744 ] 3745 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]] 3767 name = "rustc-demangle" 3768 version = "0.1.24" 3769 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3971 "base16ct", 3972 "der", 3973 "generic-array", 3974 + "pkcs8", 3975 "subtle", 3976 "zeroize", 3977 ] ··· 4365 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 4366 dependencies = [ 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", 4378 ] 4379 4380 [[package]] ··· 5252 "clap", 5253 "ctrlc", 5254 "dashmap", 5255 + "elliptic-curve", 5256 "handlebars", 5257 "hickory-resolver", 5258 + "jose-jwk", 5259 "jsonwebtoken", 5260 "metrics", 5261 "metrics-exporter-prometheus 0.17.2", 5262 + "p256", 5263 + "pkcs8", 5264 "rand 0.9.1", 5265 "reqwest", 5266 "serde",
+4
who-am-i/Cargo.toml
··· 14 clap = { version = "4.5.40", features = ["derive", "env"] } 15 ctrlc = "3.4.7" 16 dashmap = "6.1.0" 17 handlebars = { version = "6.3.2", features = ["dir_source"] } 18 hickory-resolver = "0.25.2" 19 jsonwebtoken = "9.3.1" 20 metrics = "0.24.2" 21 rand = "0.9.1" 22 reqwest = { version = "0.12.22", features = ["native-tls-vendored"] } 23 serde = { version = "1.0.219", features = ["derive"] }
··· 14 clap = { version = "4.5.40", features = ["derive", "env"] } 15 ctrlc = "3.4.7" 16 dashmap = "6.1.0" 17 + elliptic-curve = "0.13.8" 18 handlebars = { version = "6.3.2", features = ["dir_source"] } 19 hickory-resolver = "0.25.2" 20 + jose-jwk = "0.1.2" 21 jsonwebtoken = "9.3.1" 22 metrics = "0.24.2" 23 + p256 = "0.13.2" 24 + pkcs8 = "0.10.2" 25 rand = "0.9.1" 26 reqwest = { version = "0.12.22", features = ["native-tls-vendored"] } 27 serde = { version = "1.0.219", features = ["derive"] }
+38 -1
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 private key (PEM pk8 format) 19 /// 20 /// generate with: ··· 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 42 #[arg(long, action)] 43 dev: bool, 44 /// Hosts who are allowed to one-click auth ··· 57 58 let args = Args::parse(); 59 60 if args.allowed_hosts.is_empty() { 61 panic!("at least one --allowed-host host must be set"); 62 } ··· 75 serve( 76 shutdown, 77 args.app_secret, 78 tokens, 79 args.allowed_hosts, 80 args.dev, 81 )
··· 15 /// eg: `cat /dev/urandom | head -c 64 | base64` 16 #[arg(long, env)] 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>, 26 /// path to jwt private key (PEM pk8 format) 27 /// 28 /// generate with: ··· 42 /// wrap the jwk in an array, then in an object under "keys": 43 /// 44 /// { "keys": [<JWK obj>] } 45 + /// 46 + /// TODO: remove this, serve automatically 47 #[arg(long)] 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, 57 /// Enable dev mode 58 /// 59 + /// enables automatic template reloading, uses localhost oauth config, etc 60 #[arg(long, action)] 61 dev: bool, 62 /// Hosts who are allowed to one-click auth ··· 75 76 let args = Args::parse(); 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 + 94 if args.allowed_hosts.is_empty() { 95 panic!("at least one --allowed-host host must be set"); 96 } ··· 109 serve( 110 shutdown, 111 args.app_secret, 112 + args.oauth_private_key, 113 tokens, 114 + base, 115 + args.bind, 116 args.allowed_hosts, 117 args.dev, 118 )
+77 -21
who-am-i/src/oauth.rs
··· 1 use atrium_api::{agent::SessionManager, types::string::Did}; 2 use atrium_common::resolver::Resolver; 3 use atrium_identity::{ ··· 5 handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver}, 6 }; 7 use atrium_oauth::{ 8 - AtprotoLocalhostClientMetadata, AuthorizeOptions, CallbackParams, DefaultHttpClient, 9 - KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope, 10 store::{session::MemorySessionStore, state::MemoryStateStore}, 11 }; 12 use hickory_resolver::{ResolveError, TokioResolver}; 13 use serde::Deserialize; 14 use std::sync::Arc; 15 use thiserror::Error; ··· 83 } 84 85 impl OAuth { 86 - pub fn new() -> Result<Self, AuthSetupError> { 87 let http_client = Arc::new(DefaultHttpClient::default()); 88 let did_resolver = || { 89 CommonDidResolver::new(CommonDidResolverConfig { ··· 93 }; 94 let dns_txt_resolver = 95 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(), 113 }; 114 115 - let client = OAuthClient::new(client_config).map_err(AuthSetupError::AtriumClientError)?; 116 117 Ok(Self { 118 client: Arc::new(client), 119 did_resolver: Arc::new(did_resolver()), 120 }) 121 } 122 123 pub async fn begin(&self, handle: &str) -> Result<String, atrium_oauth::Error> {
··· 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; 8 use atrium_api::{agent::SessionManager, types::string::Did}; 9 use atrium_common::resolver::Resolver; 10 use atrium_identity::{ ··· 12 handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver}, 13 }; 14 use atrium_oauth::{ 15 + AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, AuthorizeOptions, 16 + CallbackParams, DefaultHttpClient, GrantType, KnownScope, OAuthClient, OAuthClientConfig, 17 + OAuthClientMetadata, OAuthResolverConfig, Scope, 18 store::{session::MemorySessionStore, state::MemoryStateStore}, 19 }; 20 + use elliptic_curve::SecretKey; 21 use hickory_resolver::{ResolveError, TokioResolver}; 22 + use jose_jwk::JwkSet; 23 + use pkcs8::DecodePrivateKey; 24 use serde::Deserialize; 25 use std::sync::Arc; 26 use thiserror::Error; ··· 94 } 95 96 impl OAuth { 97 + pub fn new(oauth_private_key: Option<PathBuf>, base: String) -> Result<Self, AuthSetupError> { 98 let http_client = Arc::new(DefaultHttpClient::default()); 99 let did_resolver = || { 100 CommonDidResolver::new(CommonDidResolverConfig { ··· 104 }; 105 let dns_txt_resolver = 106 HickoryDnsTxtResolver::new().map_err(AuthSetupError::HickoryResolverError)?; 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(), 116 }; 117 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 + }; 164 165 Ok(Self { 166 client: Arc::new(client), 167 did_resolver: Arc::new(did_resolver()), 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() 177 } 178 179 pub async fn begin(&self, handle: &str) -> Result<String, atrium_oauth::Error> {
+22 -2
who-am-i/src/server.rs
··· 1 use atrium_api::types::string::Did; 2 use axum::{ 3 Router, 4 extract::{FromRef, Json as ExtractJson, Query, State}, ··· 12 use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; 13 use axum_template::{RenderHtml, engine::Engine}; 14 use handlebars::{Handlebars, handlebars_helper}; 15 16 use serde::Deserialize; 17 use serde_json::{Value, json}; ··· 52 } 53 } 54 55 pub async fn serve( 56 shutdown: CancellationToken, 57 app_secret: String, 58 tokens: Tokens, 59 allowed_hosts: Vec<String>, 60 dev: bool, 61 ) { ··· 70 // clients have to pick up their identity-resolving tasks within this period 71 let task_pickup_expiration = Duration::from_secs(15); 72 73 - let oauth = OAuth::new().unwrap(); 74 75 let state = AppState { 76 engine: Engine::new(hbs), ··· 88 .route("/style.css", get(css)) 89 .route("/prompt", get(prompt)) 90 .route("/user-info", post(user_info)) 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") 98 .await 99 .expect("listener binding to work"); 100 ··· 297 Json(json!({ "handle": handle })).into_response() 298 } 299 } 300 } 301 302 #[derive(Debug, Deserialize)]
··· 1 use atrium_api::types::string::Did; 2 + use atrium_oauth::OAuthClientMetadata; 3 use axum::{ 4 Router, 5 extract::{FromRef, Json as ExtractJson, Query, State}, ··· 13 use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; 14 use axum_template::{RenderHtml, engine::Engine}; 15 use handlebars::{Handlebars, handlebars_helper}; 16 + use jose_jwk::JwkSet; 17 + use std::path::PathBuf; 18 19 use serde::Deserialize; 20 use serde_json::{Value, json}; ··· 55 } 56 } 57 58 + #[allow(clippy::too_many_arguments)] 59 pub async fn serve( 60 shutdown: CancellationToken, 61 app_secret: String, 62 + oauth_private_key: Option<PathBuf>, 63 tokens: Tokens, 64 + base: String, 65 + bind: String, 66 allowed_hosts: Vec<String>, 67 dev: bool, 68 ) { ··· 77 // clients have to pick up their identity-resolving tasks within this period 78 let task_pickup_expiration = Duration::from_secs(15); 79 80 + let oauth = OAuth::new(oauth_private_key, base).unwrap(); 81 82 let state = AppState { 83 engine: Engine::new(hbs), ··· 95 .route("/style.css", get(css)) 96 .route("/prompt", get(prompt)) 97 .route("/user-info", post(user_info)) 98 + .route("/client-metadata.json", get(client_metadata)) 99 .route("/auth", get(start_oauth)) 100 .route("/authorized", get(complete_oauth)) 101 .route("/disconnect", post(disconnect)) 102 + .route("/.well-known/at-jwks.json", get(at_jwks)) // todo combine jwks eps (key id is enough?) 103 .route("/.well-known/jwks.json", get(jwks)) 104 .with_state(state); 105 106 + eprintln!("starting server at http://{bind}"); 107 + let listener = TcpListener::bind(bind) 108 .await 109 .expect("listener binding to work"); 110 ··· 307 Json(json!({ "handle": handle })).into_response() 308 } 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()) 320 } 321 322 #[derive(Debug, Deserialize)]