slack status without the slack status.zzstoatzz.io/
quickslice

Merge pull request #34 from zzstoatzz/remove-weird-thing

revert weird things

authored by

nate nowack and committed by
GitHub
d56f59c7 7548698e

+2 -507
+2 -6
src/main.rs
··· 88 88 /// OAuth client metadata endpoint for production 89 89 #[get("/oauth-client-metadata.json")] 90 90 async fn client_metadata(config: web::Data<config::Config>) -> Result<HttpResponse> { 91 - // Note: This also handles /oauth-client-metadata.json?v=2 etc 92 91 let public_url = config.oauth_redirect_base.clone(); 93 92 94 93 let metadata = serde_json::json!({ 95 - "client_id": format!("{}/oauth-client-metadata.json?v=2", public_url), 94 + "client_id": format!("{}/oauth-client-metadata.json", public_url), 96 95 "client_name": "Status Sphere", 97 96 "client_uri": public_url.clone(), 98 97 "redirect_uris": [format!("{}/oauth/callback", public_url)], ··· 1509 1508 1510 1509 let oauth_config = OAuthClientConfig { 1511 1510 client_metadata: AtprotoClientMetadata { 1512 - client_id: format!( 1513 - "{}/oauth-client-metadata.json?v=2", 1514 - config.oauth_redirect_base 1515 - ), 1511 + client_id: format!("{}/oauth-client-metadata.json", config.oauth_redirect_base), 1516 1512 client_uri: Some(config.oauth_redirect_base.clone()), 1517 1513 redirect_uris: vec![format!("{}/oauth/callback", config.oauth_redirect_base)], 1518 1514 token_endpoint_auth_method: AuthMethod::None,
-102
test_metadata_validation.py
··· 1 - #!/usr/bin/env python3 2 - """ 3 - Check if Bluesky can fetch and validate our metadata 4 - """ 5 - import requests 6 - import json 7 - 8 - def test_metadata_validation(): 9 - """Test if the metadata is valid and accessible""" 10 - 11 - print("=" * 60) 12 - print("METADATA VALIDATION TEST") 13 - print("=" * 60) 14 - 15 - metadata_url = "https://zzstoatzz-status-pr-32.fly.dev/oauth-client-metadata.json" 16 - 17 - # 1. Fetch the metadata 18 - print(f"\n1. Fetching metadata from: {metadata_url}") 19 - response = requests.get(metadata_url) 20 - 21 - if response.status_code != 200: 22 - print(f"✗ Failed to fetch metadata: {response.status_code}") 23 - return 24 - 25 - metadata = response.json() 26 - print("✓ Metadata fetched successfully") 27 - 28 - # 2. Check what Bluesky would see 29 - print("\n2. Metadata content:") 30 - print(json.dumps(metadata, indent=2)) 31 - 32 - # 3. Check if the metadata is being cached 33 - print("\n3. Testing if metadata is cached...") 34 - headers = { 35 - "User-Agent": "Bluesky OAuth Client", 36 - "Accept": "application/json" 37 - } 38 - 39 - # Make multiple requests to see if it's consistent 40 - for i in range(3): 41 - r = requests.get(metadata_url, headers=headers) 42 - if r.status_code == 200: 43 - data = r.json() 44 - print(f" Request {i+1}: scope = {data['scope'][:50]}...") 45 - else: 46 - print(f" Request {i+1}: Failed with {r.status_code}") 47 - 48 - # 4. Check if there's a mismatch between what we declare and what we request 49 - print("\n4. Checking scope matching:") 50 - declared_scope = metadata['scope'] 51 - 52 - # Split scopes for comparison 53 - declared_scopes = set(declared_scope.split()) 54 - print(f" Declared scopes ({len(declared_scopes)}):") 55 - for s in declared_scopes: 56 - print(f" - {s}") 57 - 58 - # What we're trying to request 59 - requested_scope = "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 60 - requested_scopes = set(requested_scope.split()) 61 - print(f"\n Requested scopes ({len(requested_scopes)}):") 62 - for s in requested_scopes: 63 - print(f" - {s}") 64 - 65 - # Compare 66 - print("\n5. Comparison:") 67 - if declared_scopes == requested_scopes: 68 - print("✓ Scopes match exactly") 69 - else: 70 - print("✗ Scopes don't match!") 71 - missing = requested_scopes - declared_scopes 72 - if missing: 73 - print(f" Missing from metadata: {missing}") 74 - extra = declared_scopes - requested_scopes 75 - if extra: 76 - print(f" Extra in metadata: {extra}") 77 - 78 - # 6. Test if the issue is URL encoding 79 - print("\n6. URL Encoding check:") 80 - import urllib.parse 81 - 82 - encoded_scope = urllib.parse.quote(declared_scope) 83 - print(f" URL encoded scope: {encoded_scope[:100]}...") 84 - 85 - # Check if fragment is causing issues 86 - if "#" in declared_scope: 87 - print(" ⚠️ Scope contains # fragment which might need special handling") 88 - if "?" in declared_scope: 89 - print(" ⚠️ Scope contains ? query params which might need special handling") 90 - 91 - print("\n" + "=" * 60) 92 - print("HYPOTHESIS:") 93 - print("The 400 error means Bluesky can't validate our OAuth request.") 94 - print("Possible reasons:") 95 - print("1. Metadata isn't accessible to Bluesky") 96 - print("2. Scope format is incorrect") 97 - print("3. Client ID doesn't match redirect URI domain") 98 - print("4. The OAuth client needs to be registered differently") 99 - print("=" * 60) 100 - 101 - if __name__ == "__main__": 102 - test_metadata_validation()
-46
test_oauth.py
··· 1 - #!/usr/bin/env python3 2 - import requests 3 - import json 4 - 5 - # Test different scope combinations 6 - test_cases = [ 7 - { 8 - "name": "Both with fragment", 9 - "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview" 10 - }, 11 - { 12 - "name": "getFollows without fragment", 13 - "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 14 - }, 15 - { 16 - "name": "Both without fragment", 17 - "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 18 - }, 19 - { 20 - "name": "getProfile without fragment", 21 - "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview" 22 - } 23 - ] 24 - 25 - print("Testing OAuth scope combinations...") 26 - print("=" * 60) 27 - 28 - for test in test_cases: 29 - print(f"\nTest: {test['name']}") 30 - print(f"Scope: {test['scope']}") 31 - 32 - # Create mock client metadata 33 - metadata = { 34 - "client_id": "https://test.example.com/oauth-client-metadata.json", 35 - "client_name": "Test Status App", 36 - "client_uri": "https://test.example.com", 37 - "redirect_uris": ["https://test.example.com/oauth/callback"], 38 - "scope": test['scope'], 39 - "grant_types": ["authorization_code", "refresh_token"], 40 - "response_types": ["code"], 41 - "token_endpoint_auth_method": "none", 42 - "dpop_bound_access_tokens": True 43 - } 44 - 45 - print(f"Metadata valid: {json.dumps(metadata, indent=2)}") 46 - print("-" * 40)
-102
test_oauth_minimal.py
··· 1 - #!/usr/bin/env python3 2 - """ 3 - Minimal test to check if OAuth metadata and scope declarations work 4 - """ 5 - import requests 6 - import json 7 - from urllib.parse import urlencode 8 - 9 - def test_oauth_metadata(): 10 - """Test if the OAuth metadata is correctly configured""" 11 - 12 - print("=" * 60) 13 - print("OAuth Metadata and Scope Test") 14 - print("=" * 60) 15 - 16 - # Test production 17 - print("\n1. Production site metadata:") 18 - prod_url = "https://status.zzstoatzz.io/oauth-client-metadata.json" 19 - prod_response = requests.get(prod_url) 20 - if prod_response.status_code == 200: 21 - prod_metadata = prod_response.json() 22 - print(f"✓ Got metadata") 23 - print(f" Scope: {prod_metadata['scope']}") 24 - 25 - # Check which scopes are present 26 - scope = prod_metadata['scope'] 27 - if "rpc:app.bsky.actor.getProfile" in scope: 28 - has_fragment = "#bsky_appview" in scope.split("rpc:app.bsky.actor.getProfile")[1].split()[0] 29 - print(f" - getProfile: present (fragment: {has_fragment})") 30 - 31 - if "rpc:app.bsky.graph.getFollows" in scope: 32 - has_fragment = "#bsky_appview" in scope.split("rpc:app.bsky.graph.getFollows")[1] if "rpc:app.bsky.graph.getFollows" in scope else False 33 - print(f" - getFollows: present (fragment: {has_fragment})") 34 - else: 35 - print(f"✗ Failed to get metadata: {prod_response.status_code}") 36 - 37 - # Test preview 38 - print("\n2. Preview site metadata:") 39 - preview_url = "https://zzstoatzz-status-pr-32.fly.dev/oauth-client-metadata.json" 40 - preview_response = requests.get(preview_url) 41 - if preview_response.status_code == 200: 42 - preview_metadata = preview_response.json() 43 - print(f"✓ Got metadata") 44 - print(f" Scope: {preview_metadata['scope']}") 45 - 46 - # Check which scopes are present 47 - scope = preview_metadata['scope'] 48 - if "rpc:app.bsky.actor.getProfile" in scope: 49 - profile_part = scope.split("rpc:app.bsky.actor.getProfile")[1].split()[0] if len(scope.split("rpc:app.bsky.actor.getProfile")) > 1 else "" 50 - has_fragment = "#bsky_appview" in profile_part 51 - print(f" - getProfile: present (fragment: {has_fragment})") 52 - 53 - if "rpc:app.bsky.graph.getFollows" in scope: 54 - follows_part = scope.split("rpc:app.bsky.graph.getFollows")[1] if len(scope.split("rpc:app.bsky.graph.getFollows")) > 1 else "" 55 - has_fragment = "#bsky_appview" in follows_part 56 - print(f" - getFollows: present (fragment: {has_fragment})") 57 - else: 58 - print(f"✗ Failed to get metadata: {preview_response.status_code}") 59 - 60 - # Compare 61 - print("\n3. Comparison:") 62 - if prod_response.status_code == 200 and preview_response.status_code == 200: 63 - if prod_metadata['scope'] == preview_metadata['scope']: 64 - print("✓ Scopes are identical") 65 - else: 66 - print("✗ Scopes differ:") 67 - print(f" Production: {prod_metadata['scope']}") 68 - print(f" Preview: {preview_metadata['scope']}") 69 - 70 - # Test what Bluesky expects 71 - print("\n4. What Bluesky error messages tell us:") 72 - print(" - getProfile needs: rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview") 73 - print(" - getFollows needs: rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app") 74 - print(" Note: getProfile has #bsky_appview fragment, getFollows does NOT") 75 - 76 - print("\n5. OAuth authorization URL test:") 77 - client_id = "https://zzstoatzz-status-pr-32.fly.dev/oauth-client-metadata.json" 78 - redirect_uri = "https://zzstoatzz-status-pr-32.fly.dev/oauth/callback" 79 - 80 - # Build the authorization URL with the scopes 81 - scope = "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 82 - 83 - params = { 84 - "response_type": "code", 85 - "client_id": client_id, 86 - "redirect_uri": redirect_uri, 87 - "scope": scope, 88 - "state": "test" 89 - } 90 - 91 - auth_url = f"https://bsky.social/oauth/authorize?{urlencode(params)}" 92 - print(f" Authorization URL (first 150 chars):") 93 - print(f" {auth_url[:150]}...") 94 - 95 - print("\n" + "=" * 60) 96 - print("IMPORTANT:") 97 - print("The OAuth flow might be caching tokens server-side.") 98 - print("Even with correct scopes, old tokens might persist.") 99 - print("=" * 60) 100 - 101 - if __name__ == "__main__": 102 - test_oauth_metadata()
-197
test_oauth_real.py
··· 1 - #!/usr/bin/env python3 2 - """ 3 - Actually test the OAuth flow end-to-end 4 - """ 5 - import requests 6 - import json 7 - import sys 8 - from urllib.parse import urlparse, parse_qs 9 - 10 - def test_real_oauth(handle, app_password): 11 - """Test the actual OAuth flow""" 12 - 13 - print("=" * 60) 14 - print("TESTING REAL OAUTH FLOW") 15 - print("=" * 60) 16 - 17 - # Step 1: Create a session to simulate being logged into Bluesky 18 - print("\n1. Creating Bluesky session...") 19 - session = requests.Session() 20 - 21 - login_response = session.post( 22 - "https://bsky.social/xrpc/com.atproto.server.createSession", 23 - json={ 24 - "identifier": handle, 25 - "password": app_password 26 - } 27 - ) 28 - 29 - if login_response.status_code != 200: 30 - print(f"Failed to login: {login_response.text}") 31 - return 32 - 33 - login_data = login_response.json() 34 - did = login_data['did'] 35 - access_token = login_data['accessJwt'] 36 - print(f"✓ Logged in as {did}") 37 - 38 - # Step 2: Start OAuth authorization 39 - print("\n2. Starting OAuth flow...") 40 - 41 - client_id = "https://zzstoatzz-status-pr-32.fly.dev/oauth-client-metadata.json" 42 - redirect_uri = "https://zzstoatzz-status-pr-32.fly.dev/oauth/callback" 43 - 44 - # Test different scope combinations 45 - test_scopes = [ 46 - ("Current (getFollows no fragment)", "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app"), 47 - ("Both with fragment", "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview"), 48 - ("Both without fragment", "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app"), 49 - ("Just getProfile (working in prod)", "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview"), 50 - ] 51 - 52 - for scope_name, scope in test_scopes: 53 - print(f"\nTesting: {scope_name}") 54 - print(f"Scope: {scope}") 55 - 56 - auth_params = { 57 - "response_type": "code", 58 - "client_id": client_id, 59 - "redirect_uri": redirect_uri, 60 - "scope": scope, 61 - "state": "test123" 62 - } 63 - 64 - # Make the authorization request 65 - auth_response = session.get( 66 - "https://bsky.social/oauth/authorize", 67 - params=auth_params, 68 - allow_redirects=False, 69 - headers={"Authorization": f"Bearer {access_token}"} 70 - ) 71 - 72 - print(f" Status: {auth_response.status_code}") 73 - 74 - if auth_response.status_code == 400: 75 - # Try to extract error from HTML 76 - import re 77 - error_match = re.search(r'<title>(.*?)</title>', auth_response.text) 78 - if error_match: 79 - print(f" Error: {error_match.group(1)}") 80 - continue 81 - elif auth_response.status_code != 302: 82 - print(f" Unexpected status") 83 - continue 84 - 85 - # If we got here, it worked 86 - print(f" ✓ SUCCESS! This scope configuration works!") 87 - 88 - # Continue with the working scope 89 - scope = scope # Use the last tested scope 90 - 91 - print(f"Authorization response status: {auth_response.status_code}") 92 - 93 - if auth_response.status_code == 302: 94 - # Check if we got redirected with a code 95 - location = auth_response.headers.get('Location') 96 - if location: 97 - parsed = urlparse(location) 98 - params = parse_qs(parsed.query) 99 - 100 - if 'code' in params: 101 - code = params['code'][0] 102 - print(f"✓ Got authorization code: {code[:20]}...") 103 - 104 - # Step 3: Exchange code for token 105 - print("\n3. Exchanging code for token...") 106 - 107 - token_response = requests.post( 108 - "https://bsky.social/oauth/token", 109 - json={ 110 - "grant_type": "authorization_code", 111 - "code": code, 112 - "redirect_uri": redirect_uri, 113 - "client_id": client_id 114 - } 115 - ) 116 - 117 - print(f"Token exchange status: {token_response.status_code}") 118 - if token_response.status_code == 200: 119 - token_data = token_response.json() 120 - oauth_token = token_data.get('access_token') 121 - print(f"✓ Got OAuth token") 122 - 123 - # Decode the token to see what scopes it has 124 - if oauth_token: 125 - import base64 126 - parts = oauth_token.split('.') 127 - if len(parts) >= 2: 128 - payload = parts[1] 129 - # Add padding if needed 130 - padding = 4 - len(payload) % 4 131 - if padding != 4: 132 - payload += '=' * padding 133 - try: 134 - decoded = base64.urlsafe_b64decode(payload) 135 - token_payload = json.loads(decoded) 136 - print("\nToken payload:") 137 - print(json.dumps(token_payload, indent=2)) 138 - 139 - if 'scope' in token_payload: 140 - print(f"\n✓ Scopes in token: {token_payload['scope']}") 141 - else: 142 - print("\n✗ No scope field in token!") 143 - except: 144 - print("Could not decode token payload") 145 - 146 - # Step 4: Test the token 147 - print("\n4. Testing OAuth token with APIs...") 148 - 149 - # Test getProfile 150 - profile_resp = requests.get( 151 - "https://bsky.social/xrpc/app.bsky.actor.getProfile", 152 - params={"actor": did}, 153 - headers={"Authorization": f"Bearer {oauth_token}"} 154 - ) 155 - print(f"getProfile: {profile_resp.status_code}") 156 - if profile_resp.status_code != 200: 157 - print(f" Error: {profile_resp.text[:200]}") 158 - 159 - # Test getFollows 160 - follows_resp = requests.get( 161 - "https://bsky.social/xrpc/app.bsky.graph.getFollows", 162 - params={"actor": did, "limit": 1}, 163 - headers={"Authorization": f"Bearer {oauth_token}"} 164 - ) 165 - print(f"getFollows: {follows_resp.status_code}") 166 - if follows_resp.status_code != 200: 167 - print(f" Error: {follows_resp.text[:200]}") 168 - 169 - else: 170 - print(f"✗ Token exchange failed: {token_response.text}") 171 - 172 - elif 'error' in params: 173 - print(f"✗ Got error: {params.get('error')} - {params.get('error_description')}") 174 - else: 175 - print(f"✗ Unexpected redirect: {location}") 176 - else: 177 - print("✗ No redirect location") 178 - else: 179 - print(f"Response headers: {dict(auth_response.headers)}") 180 - print(f"Response body: {auth_response.text[:500]}") 181 - 182 - print("\n" + "=" * 60) 183 - print("CONCLUSION:") 184 - print("This test shows what scopes the OAuth token actually gets") 185 - print("vs what we're requesting in the metadata.") 186 - print("=" * 60) 187 - 188 - if __name__ == "__main__": 189 - if len(sys.argv) != 3: 190 - print("Usage: python test_oauth_real.py <handle> <app_password>") 191 - print("Example: python test_oauth_real.py alice.bsky.social myapppassword") 192 - sys.exit(1) 193 - 194 - handle = sys.argv[1] 195 - app_password = sys.argv[2] 196 - 197 - test_real_oauth(handle, app_password)
-54
test_scopes.js
··· 1 - #!/usr/bin/env node 2 - 3 - // Test OAuth scope validation with Bluesky 4 - const testCases = [ 5 - { 6 - name: "Both with fragment", 7 - metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview", 8 - request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview" 9 - }, 10 - { 11 - name: "getFollows without fragment in both", 12 - metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app", 13 - request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 14 - }, 15 - { 16 - name: "getFollows without fragment in request only", 17 - metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview", 18 - request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 19 - }, 20 - { 21 - name: "Both without fragment", 22 - metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app", 23 - request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 24 - } 25 - ]; 26 - 27 - console.log("OAuth Scope Test Results"); 28 - console.log("=" .repeat(60)); 29 - 30 - for (const test of testCases) { 31 - console.log(`\nTest: ${test.name}`); 32 - console.log(`Metadata scope: ${test.metadata_scope}`); 33 - console.log(`Request scope: ${test.request_scope}`); 34 - 35 - // Check if scopes match 36 - const matches = test.metadata_scope === test.request_scope; 37 - console.log(`Scopes match: ${matches ? "✓" : "✗"}`); 38 - 39 - // Check what error message would be 40 - if (test.request_scope.includes("getFollows?aud=did:web:api.bsky.app#bsky_appview")) { 41 - console.log("Expected error: Missing scope without fragment"); 42 - } else if (test.request_scope.includes("getFollows?aud=did:web:api.bsky.app")) { 43 - console.log("Expected: Should work if metadata declares it"); 44 - } 45 - 46 - console.log("-".repeat(40)); 47 - } 48 - 49 - console.log("\nCONCLUSION:"); 50 - console.log("The error message says it needs: rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app"); 51 - console.log("This is WITHOUT the #bsky_appview fragment"); 52 - console.log("\nWe should try:"); 53 - console.log("1. Metadata AND request both WITHOUT fragment for getFollows"); 54 - console.log("2. Or check if there's a different issue entirely");