interactive intro to open social at-me.zzstoatzz.io

fix: add production OAuth client metadata support

- use AtprotoClientMetadata for https URLs (production)
- use AtprotoLocalhostClientMetadata for http URLs (local dev)
- add /oauth-client-metadata.json endpoint
- fixes OAuth client creation failure in production

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

+78 -26
+1
src/main.rs
··· 32 32 .service(routes::index) 33 33 .service(routes::login) 34 34 .service(routes::callback) 35 + .service(routes::client_metadata) 35 36 .service(routes::logout) 36 37 .service(routes::restore_session) 37 38 })
+53 -26
src/oauth.rs
··· 3 3 handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig, DnsTxtResolver}, 4 4 }; 5 5 use atrium_oauth::{ 6 - AtprotoLocalhostClientMetadata, DefaultHttpClient, KnownScope, OAuthClient, 7 - OAuthClientConfig, OAuthResolverConfig, Scope, 6 + AtprotoClientMetadata, AtprotoLocalhostClientMetadata, AuthMethod, DefaultHttpClient, 7 + GrantType, KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope, 8 8 store::{session::MemorySessionStore, state::MemoryStateStore}, 9 9 }; 10 10 use hickory_resolver::{TokioAsyncResolver, config::{ResolverConfig, ResolverOpts}}; ··· 46 46 let redirect_uri = std::env::var("OAUTH_REDIRECT_URI") 47 47 .unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string()); 48 48 49 - Arc::new( 50 - OAuthClient::new(OAuthClientConfig { 51 - client_metadata: AtprotoLocalhostClientMetadata { 52 - redirect_uris: Some(vec![redirect_uri]), 53 - scopes: Some(vec![Scope::Known(KnownScope::Atproto)]), 54 - }, 55 - keys: None, 56 - resolver: OAuthResolverConfig { 57 - did_resolver: CommonDidResolver::new(CommonDidResolverConfig { 58 - plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 59 - http_client: http_client.clone(), 60 - }), 61 - handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 62 - dns_txt_resolver: dns_resolver, 63 - http_client: http_client.clone(), 64 - }), 65 - authorization_server_metadata: Default::default(), 66 - protected_resource_metadata: Default::default(), 67 - }, 68 - state_store: MemoryStateStore::default(), 69 - session_store: MemorySessionStore::default(), 70 - }) 71 - .expect("failed to create oauth client"), 72 - ) 49 + let is_production = redirect_uri.starts_with("https://"); 50 + 51 + let resolver = OAuthResolverConfig { 52 + did_resolver: CommonDidResolver::new(CommonDidResolverConfig { 53 + plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 54 + http_client: http_client.clone(), 55 + }), 56 + handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 57 + dns_txt_resolver: dns_resolver, 58 + http_client: http_client.clone(), 59 + }), 60 + authorization_server_metadata: Default::default(), 61 + protected_resource_metadata: Default::default(), 62 + }; 63 + 64 + if is_production { 65 + let base_url = redirect_uri.trim_end_matches("/oauth/callback"); 66 + Arc::new( 67 + OAuthClient::new(OAuthClientConfig { 68 + client_metadata: AtprotoClientMetadata { 69 + client_id: format!("{}/oauth-client-metadata.json", base_url), 70 + client_uri: Some(base_url.to_string()), 71 + redirect_uris: vec![redirect_uri], 72 + token_endpoint_auth_method: AuthMethod::None, 73 + grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], 74 + scopes: vec![Scope::Known(KnownScope::Atproto)], 75 + jwks_uri: None, 76 + token_endpoint_auth_signing_alg: None, 77 + }, 78 + keys: None, 79 + resolver, 80 + state_store: MemoryStateStore::default(), 81 + session_store: MemorySessionStore::default(), 82 + }) 83 + .expect("failed to create oauth client"), 84 + ) 85 + } else { 86 + Arc::new( 87 + OAuthClient::new(OAuthClientConfig { 88 + client_metadata: AtprotoLocalhostClientMetadata { 89 + redirect_uris: Some(vec![redirect_uri]), 90 + scopes: Some(vec![Scope::Known(KnownScope::Atproto)]), 91 + }, 92 + keys: None, 93 + resolver, 94 + state_store: MemoryStateStore::default(), 95 + session_store: MemorySessionStore::default(), 96 + }) 97 + .expect("failed to create oauth client"), 98 + ) 99 + } 73 100 }
+24
src/routes.rs
··· 97 97 } 98 98 } 99 99 100 + #[get("/oauth-client-metadata.json")] 101 + pub async fn client_metadata() -> HttpResponse { 102 + let base_url = std::env::var("OAUTH_REDIRECT_URI") 103 + .unwrap_or_else(|_| "http://127.0.0.1:8080/oauth/callback".to_string()) 104 + .trim_end_matches("/oauth/callback") 105 + .to_string(); 106 + 107 + let metadata = serde_json::json!({ 108 + "client_id": format!("{}/oauth-client-metadata.json", base_url), 109 + "client_name": "@me", 110 + "client_uri": base_url.clone(), 111 + "redirect_uris": [format!("{}/oauth/callback", base_url)], 112 + "scope": "atproto", 113 + "grant_types": ["authorization_code", "refresh_token"], 114 + "response_types": ["code"], 115 + "token_endpoint_auth_method": "none", 116 + "dpop_bound_access_tokens": true 117 + }); 118 + 119 + HttpResponse::Ok() 120 + .content_type("application/json") 121 + .body(metadata.to_string()) 122 + } 123 + 100 124 #[get("/logout")] 101 125 pub async fn logout(session: Session) -> HttpResponse { 102 126 session.purge();