> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Socialite - Laravel Bluesky

> Laravel Bluesky의 OAuth 인증. AT Protocol 고유의 DPoP·PAR 엔드포인트를 사용한 Socialite 설정과 사용법.

## 개요

Bluesky의 OAuth는 AT Protocol에 기반하고 있으며, GitHub나 Google 등 일반적인 Socialite 프로바이더와는 크게 다릅니다.

<Warning>
  Bluesky의 OAuth는 다른 Socialite 프로바이더와는 구현이 근본적으로 다릅니다. DPoP(Demonstrated Proof of Possession)와 PAR(Pushed Authorization Requests) 엔드포인트를 사용합니다. `client_secret`은 불필요하고, 대신 비밀 키를 사용합니다.
</Warning>

### 일반 OAuth와의 차이

| 항목          | 일반 Socialite                  | Bluesky Socialite                 |
| ----------- | ----------------------------- | --------------------------------- |
| 인증 방식       | OAuth 2.0                     | AT Protocol OAuth(DPoP)           |
| 클라이언트 식별    | `client_id` + `client_secret` | 비밀 키 + `client-metadata.json` URL |
| `client_id` | 등록된 고정 문자열                    | `client-metadata.json`의 URL       |
| 사용자 식별자     | 이메일 주소 등                      | DID(`did:plc:...`) 또는 핸들          |
| 로그인 힌트      | 불필요                           | 핸들 / DID / PDS URL을 전달 가능         |

### 인증 흐름

```mermaid theme={null}
sequenceDiagram
    participant User as 사용자
    participant App as Laravel<br>앱
    participant Bluesky as Bluesky<br>서버

    User->>App: 로그인 폼 전송(핸들 입력)
    App->>Bluesky: PAR 요청(login_hint 포함)
    Bluesky-->>App: request_uri
    App->>Bluesky: 인가 엔드포인트로 리디렉트
    User->>Bluesky: Bluesky에서 로그인 승인
    Bluesky-->>App: 콜백(code + iss)
    App->>Bluesky: DPoP를 사용한 토큰 교환
    Bluesky-->>App: 액세스 토큰 + OAuthSession
    App->>User: 로그인 완료
```

## 설치와 설정

### 비밀 키 생성

먼저 비밀 키를 생성합니다. 이는 Bluesky에 등록하지 않고도 진행할 수 있습니다.

```bash theme={null}
php artisan bluesky:new-private-key
```

```
Please set this private key in .env

BLUESKY_OAUTH_PRIVATE_KEY="...url-safe base64 encoded key..."
```

출력된 값을 `.env`에 복사합니다.

```dotenv theme={null}
BLUESKY_OAUTH_PRIVATE_KEY="..."
```

<Info>
  Bluesky의 경우 `client_id`나 `client_secret` 등록이 불필요합니다. 비밀 키 설정만으로도 OAuth 인증을 사용할 수 있습니다.
</Info>

### 기본 OAuth 스코프

패키지는 3가지 주요 사용 사례를 지원하는 기본 OAuth 스코프로 설정되어 있습니다.

1. **Socialite 로그인** — `atproto`, `account:email`, `include:app.bsky.authViewAll`로 사용자 인증과 이메일 액세스를 활성화합니다
2. **게시** — `include:app.bsky.authCreatePosts`와 `blob:*/*`로 게시 작성과 이미지·동영상 업로드를 허용합니다
3. **DM 알림** — `rpc:chat.bsky.convo.sendMessage`와 `rpc:chat.bsky.convo.getConvoForMembers`로 알림용 다이렉트 메시지 전송을 활성화합니다

`BLUESKY_OAUTH_SCOPE` 환경 변수를 설정하여 스코프를 커스터마이즈할 수 있습니다.

```dotenv theme={null}
BLUESKY_OAUTH_SCOPE="atproto account:email include:app.bsky.authViewAll"
```

이용 가능한 스코프의 상세는 [AT Protocol Permission Requests 문서](https://atproto.com/guides/permission-requests)를 참조하세요.

### 로컬 개발

기본으로 `http://localhost`와 `http://127.0.0.1:8000/`이 설정되어 있으므로, 로컬 개발에는 추가 설정이 불필요합니다.

```dotenv theme={null}
# 포트를 변경한 경우에만 설정
BLUESKY_CLIENT_ID=
BLUESKY_REDIRECT=http://127.0.0.1:8080/
```

### 프로덕션 환경

라우트 이름 `bluesky.oauth.redirect`가 존재한다면 `.env`에 별도 설정은 불필요합니다. 기본 라우트 이름을 변경한 경우에는 설정합니다.

```dotenv theme={null}
BLUESKY_REDIRECT=/bluesky/callback
```

## 라우트 설정

콜백 라우트의 이름은 `bluesky.oauth.redirect`를 권장합니다. 패키지가 이 이름을 내부적으로 사용합니다.

```php theme={null}
// routes/web.php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SocialiteController;

Route::get('login', [SocialiteController::class, 'login'])->name('login');
Route::match(['get', 'post'], 'redirect', [SocialiteController::class, 'redirect']);
Route::get('bluesky/callback', [SocialiteController::class, 'callback'])
     ->name('bluesky.oauth.redirect');
```

### 로컬 개발에서의 콜백 처리

로컬 개발 중에는 Bluesky의 콜백 URL이 `http://127.0.0.1:8000/`으로 고정됩니다. 라우트 단위에서 분기 처리하면 편리합니다.

```php theme={null}
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/', function (Request $request) {
    if (app()->isLocal() && $request->has('iss')) {
        return to_route('bluesky.oauth.redirect', $request->query());
    }

    // ...
});
```

## 컨트롤러 구현

```php theme={null}
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
use Revolution\Bluesky\Session\OAuthSession;

class SocialiteController extends Controller
{
    public function login(Request $request)
    {
        // 로그인 힌트를 입력하는 폼 페이지
        return view('login');
    }

    public function redirect(Request $request)
    {
        // 핸들, DID, 또는 PDS URL을 login_hint로 전달 가능(생략 가능)
        $hint = $request->input('login_hint');
        $request->session()->put('hint', $hint);

        return Socialite::driver('bluesky')
                        ->hint($hint)
                        ->redirect();
    }

    public function callback(Request $request)
    {
        if ($request->missing('code')) {
            // 개발 중 디버그용. 프로덕션에서는 적절한 에러 처리로 대체할 것
            dd($request);
        }

        $hint = $request->session()->pull('hint');

        /** @var \Laravel\Socialite\Two\User $user */
        $user = Socialite::driver('bluesky')
                         ->hint($hint)
                         ->user();

        /** @var OAuthSession $session */
        $session = $user->session;

        // OAuthSession을 Laravel 세션에 저장
        $request->session()->put('bluesky_session', $session->toArray());

        $loginUser = User::updateOrCreate([
            'did' => $session->did(),
        ], [
            'iss'           => $session->issuer(),
            'handle'        => $session->handle(),
            'name'          => $session->displayName(),
            'avatar'        => $session->avatar(),
            'access_token'  => $session->token(),
            'refresh_token' => $session->refresh(),
        ]);

        auth()->login($loginUser, true);

        return to_route('bluesky.dashboard');
    }
}
```

## 사용자 정보(OAuthSession)

`$user->session`으로 취득할 수 있는 `OAuthSession`의 주요 메서드를 소개합니다.

| 메서드             | 설명          | 예                     |
| --------------- | ----------- | --------------------- |
| `did()`         | Bluesky DID | `did:plc:xxxxxx`      |
| `handle()`      | Bluesky 핸들  | `alice.bsky.social`   |
| `displayName()` | 표시 이름       | `Alice`               |
| `avatar()`      | 아바터 URL     | `https://...`         |
| `issuer()`      | PDS의 URL    | `https://bsky.social` |
| `token()`       | 액세스 토큰      |                       |
| `refresh()`     | 리프레시 토큰     |                       |

모든 속성을 확인하려면 `toArray()`를 사용합니다.

```php theme={null}
/** @var OAuthSession $session */
dump($session->toArray());
```

## 데이터베이스 설정

`users` 테이블에 Bluesky 고유의 컬럼을 추가합니다. DID가 Bluesky 사용자의 고유 식별자입니다.

```php theme={null}
// database/migrations/xxxx_add_bluesky_columns_to_users_table.php

Schema::table('users', function (Blueprint $table) {
    $table->string('did')->nullable()->unique();
    $table->string('iss')->nullable();
    $table->string('handle')->nullable();
    $table->string('avatar')->nullable();
    $table->text('access_token')->nullable();
    $table->text('refresh_token')->nullable();
});
```

## OAuthSession의 재이용

세션에 저장한 OAuthSession을 사용해 API를 호출할 수 있습니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;

$session = OAuthSession::create(session('bluesky_session'));

$timeline = Bluesky::withToken($session)->getTimeline();
```

Job이나 Console 등 Laravel 세션을 사용할 수 없는 경우에는 DB에서 데이터를 취득해 OAuthSession을 구성합니다.

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;

$session = OAuthSession::create([
    'did'           => $user->did,
    'refresh_token' => $user->refresh_token,
    // bsky.social 이외의 계정은 iss도 지정
    // 'iss'        => $user->iss,
]);

$timeline = Bluesky::withToken($session)
                   ->refreshSession()
                   ->getTimeline();
```

## 토큰 자동 갱신

리프레시 토큰은 한 번밖에 사용할 수 없으므로, 갱신 후에는 반드시 DB에 다시 저장해야 합니다. `OAuthSessionUpdated` 이벤트를 사용합니다.

```bash theme={null}
php artisan make:listener OAuthSessionUpdatedListener
```

```php theme={null}
namespace App\Listeners;

use App\Models\User;
use Revolution\Bluesky\Events\OAuthSessionUpdated;

class OAuthSessionUpdatedListener
{
    public function handle(OAuthSessionUpdated $event): void
    {
        if (empty($event->session->did())) {
            return;
        }

        session()->put('bluesky_session', $event->session->toArray());

        User::firstWhere('did', $event->session->did())
            ->fill([
                'iss'           => $event->session->issuer(),
                'handle'        => $event->session->handle(),
                'name'          => $event->session->displayName(),
                'avatar'        => $event->session->avatar(),
                'access_token'  => $event->session->token(),
                'refresh_token' => $event->session->refresh(),
            ])->save();
    }
}
```

리프레시 개시 시점에는 `OAuthSessionRefreshing` 이벤트도 발행됩니다. 이 시점에서 `refresh_token`이 무효화되므로, DB에서 삭제해 두면 안전합니다.

```php theme={null}
use Revolution\Bluesky\Events\OAuthSessionRefreshing;

public function handle(OAuthSessionRefreshing $event): void
{
    if (empty($event->session->did())) {
        return;
    }

    User::firstWhere('did', $event->session->did())
        ->fill(['refresh_token' => ''])
        ->save();
}
```

## WithBluesky 트레이트

User 모델에 `WithBluesky` 트레이트를 추가하고 `tokenForBluesky()`를 구현하면, `$user->bluesky()`로 인증된 클라이언트를 취득할 수 있습니다.

```php theme={null}
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Revolution\Bluesky\Session\OAuthSession;
use Revolution\Bluesky\Traits\WithBluesky;

class User extends Authenticatable
{
    use WithBluesky;

    protected function tokenForBluesky(): OAuthSession
    {
        return OAuthSession::create([
            'did'           => $this->did,
            'refresh_token' => $this->refresh_token,
            'iss'           => $this->iss,
        ]);
    }
}
```

```php theme={null}
$profile = auth()->user()
                 ->bluesky()
                 ->refreshSession()
                 ->getProfile();
```

## client-metadata 커스터마이즈

패키지는 `bluesky.oauth.client-metadata`와 `bluesky.oauth.jwks` 라우트를 자동으로 정의합니다. 일반적으로 변경할 필요는 없지만, `OAuthConfig`로 커스터마이즈할 수 있습니다.

```php theme={null}
// AppServiceProvider

use Revolution\Bluesky\Socialite\OAuthConfig;

public function boot(): void
{
    OAuthConfig::clientMetadataUsing(function () {
        return collect(config('bluesky.oauth.metadata'))
            ->merge([
                'client_id'    => route('bluesky.oauth.client-metadata'),
                'jwks_uri'     => route('bluesky.oauth.jwks'),
                'redirect_uris' => [url('bluesky/callback')],
            ])
            ->reject(fn ($item) => is_null($item))
            ->toArray();
    });
}
```

## 미인증 시의 동작

`OAuthSession`이 null이거나 리프레시 토큰이 없는 경우, `Unauthenticated` 예외가 throw되고 `login` 라우트로 리디렉트됩니다.

```php theme={null}
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->redirectGuestsTo('/bluesky/login');
})
```

<Info>
  Source: [docs/socialite.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/socialite.md)
</Info>


## Related topics

- [Laravel Bluesky](/ko/packages/laravel-bluesky/index.md)
- [Basic client - Laravel Bluesky](/ko/packages/laravel-bluesky/basic-client.md)
- [Laravel Socialite (소셜 인증)](/ko/socialite.md)
- [봇 튜토리얼 - Laravel Bluesky](/ko/packages/laravel-bluesky/bot-tutorial.md)
- [인증 방식 비교 - Laravel Bluesky](/ko/packages/laravel-bluesky/authentication.md)
