generateClientId(); } /** * Check if this is a localhost development client. */ public function isLocalhost(): bool { return str_starts_with($this->getClientId(), 'http://localhost'); } /** * Get the redirect URIs. * * For localhost development, redirect URIs must use 127.0.0.1 * (not localhost) and can include a port number. * * @return array */ public function getRedirectUris(): array { $uris = config('client.client.redirect_uris', []); if (! empty($uris)) { return $uris; } // Default redirect URI based on environment if ($this->isLocalhost()) { // For localhost, use 127.0.0.1 return ['http://127.0.0.1']; } // For production, use app URL return [config('client.client.url').'/auth/atp/callback']; } /** * Get the OAuth scopes. * * @return array */ public function getScopes(): array { return config('client.client.scopes', ['atproto', 'transition:generic']); } /** * Get the client metadata as an array. * * This is the structure served at the client_id URL. * * @return array */ public function toArray(): array { return [ 'client_id' => $this->getClientId(), 'client_name' => config('client.client.name'), 'client_uri' => config('client.client.url'), 'redirect_uris' => $this->getRedirectUris(), 'scope' => implode(' ', $this->getScopes()), 'grant_types' => [ 'authorization_code', 'refresh_token', ], 'response_types' => ['code'], 'token_endpoint_auth_method' => 'none', 'application_type' => 'web', 'dpop_bound_access_tokens' => true, ]; } /** * Generate client_id from app configuration. * * In production, points to the package's client-metadata.json endpoint. * For localhost detection, checks if app URL contains localhost or .test. * * For localhost clients, the client_id includes query parameters for * redirect_uri and scope since there's no metadata document to fetch. * * @see https://atproto.com/specs/oauth#clients */ protected function generateClientId(): string { $appUrl = config('client.client.url') ?? config('app.url'); $host = parse_url($appUrl, PHP_URL_HOST); // Detect local development environments if ($this->isLocalDevelopment($host)) { return $this->buildLocalhostClientId(); } // Production: point to client metadata endpoint $prefix = config('client.oauth.prefix', '/atp/oauth/'); return rtrim($appUrl, '/').rtrim($prefix, '/').'/client-metadata.json'; } /** * Build localhost client_id with query parameters. * * For localhost clients, metadata is passed via query parameters: * - redirect_uri: The callback URL (using 127.0.0.1) * - scope: Space-separated list of requested scopes */ protected function buildLocalhostClientId(): string { $params = []; // Add redirect URI $redirectUris = config('client.client.redirect_uris', []); if (! empty($redirectUris)) { $params['redirect_uri'] = $redirectUris[0]; } else { $params['redirect_uri'] = 'http://127.0.0.1'; } // Add scopes $scopes = config('client.client.scopes', ['atproto']); $params['scope'] = implode(' ', $scopes); return 'http://localhost?'.http_build_query($params); } /** * Check if the host indicates a local development environment. */ protected function isLocalDevelopment(?string $host): bool { if (! $host) { return false; } return $host === 'localhost' || $host === '127.0.0.1' || str_ends_with($host, '.localhost') || str_ends_with($host, '.test') || str_ends_with($host, '.local'); } }