Laravel AT Protocol Client (alpha & unstable)
at dev 5.3 kB view raw
1<?php 2 3namespace SocialDept\AtpClient\Auth; 4 5/** 6 * Manages OAuth client metadata for AT Protocol authentication. 7 * 8 * The client_id in atproto OAuth is a URL that serves as both the unique 9 * identifier and the location of the client metadata document. 10 * 11 * For production: Use an HTTPS URL pointing to your client metadata. 12 * For localhost: Use exactly 'http://localhost' (no port). 13 * 14 * @see https://atproto.com/specs/oauth#clients 15 */ 16class ClientMetadataManager 17{ 18 /** 19 * Get the client ID (URL to client metadata document). 20 * 21 * For production clients, this is an HTTPS URL like: 22 * 'https://example.com/oauth/client-metadata.json' 23 * 24 * For localhost development, this must be exactly 'http://localhost' 25 * (no port number allowed per atproto spec). 26 */ 27 public function getClientId(): string 28 { 29 $clientId = config('client.client.client_id'); 30 31 if ($clientId) { 32 return $clientId; 33 } 34 35 // Fall back to auto-generated client_id based on app URL 36 return $this->generateClientId(); 37 } 38 39 /** 40 * Check if this is a localhost development client. 41 */ 42 public function isLocalhost(): bool 43 { 44 return str_starts_with($this->getClientId(), 'http://localhost'); 45 } 46 47 /** 48 * Get the redirect URIs. 49 * 50 * For localhost development, redirect URIs must use 127.0.0.1 51 * (not localhost) and can include a port number. 52 * 53 * @return array<string> 54 */ 55 public function getRedirectUris(): array 56 { 57 $uris = config('client.client.redirect_uris', []); 58 59 if (! empty($uris)) { 60 return $uris; 61 } 62 63 // Default redirect URI based on environment 64 if ($this->isLocalhost()) { 65 // For localhost, use 127.0.0.1 66 return ['http://127.0.0.1']; 67 } 68 69 // For production, use app URL 70 return [config('client.client.url').'/auth/atp/callback']; 71 } 72 73 /** 74 * Get the OAuth scopes. 75 * 76 * @return array<string> 77 */ 78 public function getScopes(): array 79 { 80 return config('client.client.scopes', ['atproto', 'transition:generic']); 81 } 82 83 /** 84 * Get the client metadata as an array. 85 * 86 * This is the structure served at the client_id URL. 87 * 88 * @return array<string, mixed> 89 */ 90 public function toArray(): array 91 { 92 return [ 93 'client_id' => $this->getClientId(), 94 'client_name' => config('client.client.name'), 95 'client_uri' => config('client.client.url'), 96 'redirect_uris' => $this->getRedirectUris(), 97 'scope' => implode(' ', $this->getScopes()), 98 'grant_types' => [ 99 'authorization_code', 100 'refresh_token', 101 ], 102 'response_types' => ['code'], 103 'token_endpoint_auth_method' => 'none', 104 'application_type' => 'web', 105 'dpop_bound_access_tokens' => true, 106 ]; 107 } 108 109 /** 110 * Generate client_id from app configuration. 111 * 112 * In production, points to the package's client-metadata.json endpoint. 113 * For localhost detection, checks if app URL contains localhost or .test. 114 * 115 * For localhost clients, the client_id includes query parameters for 116 * redirect_uri and scope since there's no metadata document to fetch. 117 * 118 * @see https://atproto.com/specs/oauth#clients 119 */ 120 protected function generateClientId(): string 121 { 122 $appUrl = config('client.client.url') ?? config('app.url'); 123 $host = parse_url($appUrl, PHP_URL_HOST); 124 125 // Detect local development environments 126 if ($this->isLocalDevelopment($host)) { 127 return $this->buildLocalhostClientId(); 128 } 129 130 // Production: point to client metadata endpoint 131 $prefix = config('client.oauth.prefix', '/atp/oauth/'); 132 133 return rtrim($appUrl, '/').rtrim($prefix, '/').'/client-metadata.json'; 134 } 135 136 /** 137 * Build localhost client_id with query parameters. 138 * 139 * For localhost clients, metadata is passed via query parameters: 140 * - redirect_uri: The callback URL (using 127.0.0.1) 141 * - scope: Space-separated list of requested scopes 142 */ 143 protected function buildLocalhostClientId(): string 144 { 145 $params = []; 146 147 // Add redirect URI 148 $redirectUris = config('client.client.redirect_uris', []); 149 if (! empty($redirectUris)) { 150 $params['redirect_uri'] = $redirectUris[0]; 151 } else { 152 $params['redirect_uri'] = 'http://127.0.0.1'; 153 } 154 155 // Add scopes 156 $scopes = config('client.client.scopes', ['atproto']); 157 $params['scope'] = implode(' ', $scopes); 158 159 return 'http://localhost?'.http_build_query($params); 160 } 161 162 /** 163 * Check if the host indicates a local development environment. 164 */ 165 protected function isLocalDevelopment(?string $host): bool 166 { 167 if (! $host) { 168 return false; 169 } 170 171 return $host === 'localhost' 172 || $host === '127.0.0.1' 173 || str_ends_with($host, '.localhost') 174 || str_ends_with($host, '.test') 175 || str_ends_with($host, '.local'); 176 } 177}