Laravel AT Protocol Client (alpha & unstable)
1# OAuth Scopes 2 3The AT Protocol uses OAuth scopes to control what actions an application can perform on behalf of a user. AtpClient provides attributes for documenting scope requirements on endpoints. 4 5> **Note:** The `#[ScopedEndpoint]` and `#[PublicEndpoint]` attributes currently serve as documentation only. Runtime scope validation and enforcement will be implemented in a future release. Using these attributes correctly now ensures forward compatibility. 6 7## Quick Reference 8 9### Scope Enum 10 11```php 12use SocialDept\AtpClient\Enums\Scope; 13 14// Transition scopes (current AT Protocol scopes) 15Scope::Atproto // 'atproto' - Full access 16Scope::TransitionGeneric // 'transition:generic' - General API access 17Scope::TransitionEmail // 'transition:email' - Email access 18Scope::TransitionChat // 'transition:chat.bsky' - Chat access 19 20// Granular scope builders (future AT Protocol scopes) 21Scope::repo('app.bsky.feed.post', ['create', 'delete']) // Record operations 22Scope::rpc('app.bsky.feed.getTimeline') // RPC endpoint access 23Scope::blob('image/*') // Blob upload access 24Scope::account('email') // Account attribute access 25Scope::identity('handle') // Identity attribute access 26``` 27 28### ScopedEndpoint Attribute 29 30```php 31use SocialDept\AtpClient\Attributes\ScopedEndpoint; 32use SocialDept\AtpClient\Enums\Scope; 33 34#[ScopedEndpoint(Scope::TransitionGeneric)] 35public function getTimeline(): GetTimelineResponse 36{ 37 // Method implementation 38} 39``` 40 41## Understanding AT Protocol Scopes 42 43### Current Transition Scopes 44 45The AT Protocol is currently in a transition period where broad "transition scopes" are used: 46 47| Scope | Description | 48|-------|-------------| 49| `atproto` | Full access to the AT Protocol | 50| `transition:generic` | General API access for most operations | 51| `transition:email` | Access to email-related operations | 52| `transition:chat.bsky` | Access to Bluesky chat features | 53 54### Future Granular Scopes 55 56The AT Protocol is moving toward granular scopes that provide fine-grained access control: 57 58```php 59// Record operations 60'repo:app.bsky.feed.post' // All operations on posts 61'repo:app.bsky.feed.post?action=create' // Only create posts 62'repo:app.bsky.feed.like?action=create&action=delete' // Create or delete likes 63'repo:*' // All collections, all actions 64 65// RPC endpoint access 66'rpc:app.bsky.feed.getTimeline' // Access to timeline endpoint 67'rpc:app.bsky.feed.*' // All feed endpoints 68 69// Blob operations 70'blob:image/*' // Upload images 71'blob:*/*' // Upload any blob type 72 73// Account and identity 74'account:email' // Access email 75'identity:handle' // Manage handle 76``` 77 78## The ScopedEndpoint Attribute 79 80The `#[ScopedEndpoint]` attribute documents scope requirements on methods that require authentication. 81 82### Basic Usage 83 84```php 85<?php 86 87namespace App\Atp; 88 89use SocialDept\AtpClient\Attributes\ScopedEndpoint; 90use SocialDept\AtpClient\Client\Requests\Request; 91use SocialDept\AtpClient\Enums\Scope; 92 93class CustomClient extends Request 94{ 95 #[ScopedEndpoint(Scope::TransitionGeneric)] 96 public function getTimeline(): array 97 { 98 return $this->atp->client->get('app.bsky.feed.getTimeline')->json(); 99 } 100} 101``` 102 103### With Granular Scope 104 105Document the future granular scope that will replace the transition scope: 106 107```php 108#[ScopedEndpoint( 109 Scope::TransitionGeneric, 110 granular: 'rpc:app.bsky.feed.getTimeline' 111)] 112public function getTimeline(): GetTimelineResponse 113{ 114 // ... 115} 116``` 117 118### With Description 119 120Add a human-readable description for documentation: 121 122```php 123#[ScopedEndpoint( 124 Scope::TransitionGeneric, 125 granular: 'rpc:app.bsky.feed.getTimeline', 126 description: 'Access to the user\'s home timeline' 127)] 128public function getTimeline(): GetTimelineResponse 129{ 130 // ... 131} 132``` 133 134### Multiple Scopes (AND Logic) 135 136When a method requires multiple scopes, all must be present: 137 138```php 139#[ScopedEndpoint([Scope::TransitionGeneric, Scope::TransitionEmail])] 140public function getEmailPreferences(): array 141{ 142 // Requires BOTH scopes 143} 144``` 145 146### Multiple Attributes (OR Logic) 147 148Use multiple attributes for alternative scope requirements: 149 150```php 151#[ScopedEndpoint(Scope::Atproto)] 152#[ScopedEndpoint(Scope::TransitionGeneric)] 153public function getProfile(string $actor): ProfileViewDetailed 154{ 155 // Either scope satisfies the requirement 156} 157``` 158 159## Scope Enforcement (Planned) 160 161> **Coming Soon:** Runtime scope enforcement is not yet implemented. The following documentation describes planned functionality for a future release. 162 163### Configuration 164 165Configure scope enforcement in `config/client.php` or via environment variables: 166 167```php 168'scope_enforcement' => ScopeEnforcementLevel::Permissive, 169``` 170 171| Level | Behavior | 172|-------|----------| 173| `Strict` | Throws `MissingScopeException` if required scopes are missing | 174| `Permissive` | Logs a warning but attempts the request anyway | 175 176Set via environment variable: 177 178```env 179ATP_SCOPE_ENFORCEMENT=strict 180``` 181 182### Programmatic Scope Checking 183 184Check scopes programmatically using the `ScopeChecker`: 185 186```php 187use SocialDept\AtpClient\Auth\ScopeChecker; 188use SocialDept\AtpClient\Facades\Atp; 189 190$checker = app(ScopeChecker::class); 191$session = Atp::as($did)->client->session(); 192 193// Check if session has a scope 194if ($checker->hasScope($session, Scope::TransitionGeneric)) { 195 // Session has the scope 196} 197 198// Check multiple scopes 199if ($checker->check($session, [Scope::TransitionGeneric, Scope::TransitionEmail])) { 200 // Session has ALL required scopes 201} 202 203// Check and fail if missing (respects enforcement level) 204$checker->checkOrFail($session, [Scope::TransitionGeneric]); 205 206// Check repo scope for specific action 207if ($checker->checkRepoScope($session, 'app.bsky.feed.post', 'create')) { 208 // Can create posts 209} 210``` 211 212### Granular Pattern Matching 213 214The scope checker supports wildcard patterns: 215 216```php 217// Check if session can access any feed endpoint 218$checker->matchesGranular($session, 'rpc:app.bsky.feed.*'); 219 220// Check if session can upload images 221$checker->matchesGranular($session, 'blob:image/*'); 222 223// Check if session has any repo access 224$checker->matchesGranular($session, 'repo:*'); 225``` 226 227## Route Middleware (Planned) 228 229> **Coming Soon:** Route middleware is not yet implemented. The following documentation describes planned functionality for a future release. 230 231Protect Laravel routes based on ATP session scopes: 232 233```php 234use Illuminate\Support\Facades\Route; 235 236// Single scope 237Route::get('/timeline', TimelineController::class) 238 ->middleware('atp.scope:transition:generic'); 239 240// Multiple scopes (AND logic) 241Route::get('/email-settings', EmailSettingsController::class) 242 ->middleware('atp.scope:transition:generic,transition:email'); 243``` 244 245### Middleware Configuration 246 247Configure middleware behavior in `config/client.php`: 248 249```php 250'scope_authorization' => [ 251 // What to do when scope check fails 252 'failure_action' => ScopeAuthorizationFailure::Abort, // abort, redirect, or exception 253 254 // Where to redirect (when failure_action is 'redirect') 255 'redirect_to' => '/login', 256], 257``` 258 259| Failure Action | Behavior | 260|----------------|----------| 261| `Abort` | Returns 403 Forbidden response | 262| `Redirect` | Redirects to configured URL | 263| `Exception` | Throws `ScopeAuthorizationException` | 264 265Set via environment variables: 266 267```env 268ATP_SCOPE_FAILURE_ACTION=redirect 269ATP_SCOPE_REDIRECT=/auth/login 270``` 271 272### User Model Integration 273 274For the middleware to work, your User model must implement `HasAtpSession`: 275 276```php 277<?php 278 279namespace App\Models; 280 281use Illuminate\Foundation\Auth\User as Authenticatable; 282use SocialDept\AtpClient\Contracts\HasAtpSession; 283 284class User extends Authenticatable implements HasAtpSession 285{ 286 public function getAtpDid(): ?string 287 { 288 return $this->atp_did; 289 } 290} 291``` 292 293## Public Mode and Scopes 294 295Methods marked with `#[PublicEndpoint]` can be called without authentication using `Atp::public()`: 296 297```php 298// Public mode - no authentication required 299$client = Atp::public('https://public.api.bsky.app'); 300$client->bsky->actor->getProfile('someone.bsky.social'); // Works without auth 301 302// Authenticated mode - for endpoints requiring scopes 303$client = Atp::as($did); 304$client->bsky->feed->getTimeline(); // Requires transition:generic scope 305``` 306 307Methods with `#[PublicEndpoint]` work in both modes, while methods with `#[ScopedEndpoint]` require authentication. 308 309## Exception Handling (Planned) 310 311> **Coming Soon:** These exceptions will be thrown when scope enforcement is implemented in a future release. 312 313### MissingScopeException 314 315Will be thrown when required scopes are missing and enforcement is strict: 316 317```php 318use SocialDept\AtpClient\Exceptions\MissingScopeException; 319 320try { 321 $timeline = $client->bsky->feed->getTimeline(); 322} catch (MissingScopeException $e) { 323 $missing = $e->getMissingScopes(); // Scopes that are missing 324 $granted = $e->getGrantedScopes(); // Scopes the session has 325 326 // Handle missing scope 327} 328``` 329 330### ScopeAuthorizationException 331 332Will be thrown by middleware when route access is denied: 333 334```php 335use SocialDept\AtpClient\Exceptions\ScopeAuthorizationException; 336 337try { 338 // Route protected by atp.scope middleware 339} catch (ScopeAuthorizationException $e) { 340 $required = $e->getRequiredScopes(); 341 $granted = $e->getGrantedScopes(); 342 $message = $e->getMessage(); 343} 344``` 345 346## Best Practices 347 348### 1. Document All Scope Requirements 349 350Always add `#[ScopedEndpoint]` to methods that require authentication: 351 352```php 353#[ScopedEndpoint( 354 Scope::TransitionGeneric, 355 granular: 'rpc:app.bsky.feed.getTimeline', 356 description: 'Fetches the authenticated user\'s home timeline' 357)] 358public function getTimeline(): GetTimelineResponse 359``` 360 361### 2. Use the Scope Enum 362 363Prefer the `Scope` enum over string literals for type safety: 364 365```php 366// Good 367#[ScopedEndpoint(Scope::TransitionGeneric)] 368 369// Avoid 370#[ScopedEndpoint('transition:generic')] 371``` 372 373### 3. Request Minimal Scopes 374 375When implementing OAuth, request only the scopes your application needs: 376 377```php 378$authUrl = Atp::oauth()->getAuthorizationUrl([ 379 'scope' => 'atproto transition:generic', 380]); 381``` 382 383### 4. Handle Missing Scopes Gracefully 384 385Check for scope availability before attempting operations: 386 387```php 388$checker = app(ScopeChecker::class); 389$session = $client->client->session(); 390 391if ($checker->hasScope($session, Scope::TransitionChat)) { 392 $conversations = $client->chat->getConversations(); 393} else { 394 // Inform user they need to re-authorize with chat scope 395} 396``` 397 398### 5. Use Permissive Mode in Development 399 400Start with permissive enforcement during development, then switch to strict for production: 401 402```env 403# .env.local 404ATP_SCOPE_ENFORCEMENT=permissive 405 406# .env.production 407ATP_SCOPE_ENFORCEMENT=strict 408```