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```