Saltar al contenido principal

Descripción general

El OAuth de Bluesky se basa en AT Protocol y difiere considerablemente de los proveedores habituales de Socialite como GitHub o Google.
La implementación del OAuth de Bluesky es fundamentalmente distinta a la de otros proveedores de Socialite. Usa DPoP (Demonstrated Proof of Possession) y el endpoint PAR (Pushed Authorization Requests). No se necesita client_secret; en su lugar se utiliza una clave privada.

Diferencias con OAuth convencional

ElementoSocialite convencionalBluesky Socialite
Método de autenticaciónOAuth 2.0AT Protocol OAuth (DPoP)
Identificación del clienteclient_id + client_secretClave privada + URL de client-metadata.json
client_idCadena fija registradaURL de client-metadata.json
Identificador de usuarioCorreo electrónico, etc.DID (did:plc:...) o handle
Login hintNo necesarioSe puede pasar handle / DID / URL del PDS

Flujo de autenticación

Instalación y configuración

Crear la clave privada

Genera primero la clave privada. Este paso se puede hacer sin registrar nada en Bluesky.
php artisan bluesky:new-private-key
Please set this private key in .env

BLUESKY_OAUTH_PRIVATE_KEY="...url-safe base64 encoded key..."
Copia el valor generado en .env.
BLUESKY_OAUTH_PRIVATE_KEY="..."
En Bluesky no es necesario registrar client_id ni client_secret. Basta con configurar la clave privada para poder usar la autenticación OAuth.

Scopes OAuth por defecto

El paquete se configura con scopes OAuth por defecto que cubren tres casos de uso principales.
  1. Login con Socialite — con atproto, account:email e include:app.bsky.authViewAll se habilita la autenticación del usuario y el acceso al correo.
  2. Publicaciones — con include:app.bsky.authCreatePosts y blob:*/* se permite crear publicaciones y subir imágenes/vídeos.
  3. Notificaciones DM — con rpc:chat.bsky.convo.sendMessage y rpc:chat.bsky.convo.getConvoForMembers se habilita el envío de DMs para notificaciones.
Puedes personalizar los scopes configurando la variable de entorno BLUESKY_OAUTH_SCOPE.
BLUESKY_OAUTH_SCOPE="atproto account:email include:app.bsky.authViewAll"
Para más detalles sobre los scopes disponibles, consulta la documentación de AT Protocol Permission Requests.

Desarrollo local

Como por defecto ya están configurados http://localhost y http://127.0.0.1:8000/, no se necesita configuración adicional para desarrollo local.
# Configura solo si has cambiado el puerto
BLUESKY_CLIENT_ID=
BLUESKY_REDIRECT=http://127.0.0.1:8080/

Entorno de producción

Si existe la ruta con el nombre bluesky.oauth.redirect, no hace falta configurar .env. Si has cambiado el nombre de la ruta por defecto, configúralo aquí.
BLUESKY_REDIRECT=/bluesky/callback

Configuración de rutas

Se recomienda que el nombre de la ruta de callback sea bluesky.oauth.redirect. El paquete utiliza internamente este nombre.
// 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');

Gestión del callback en desarrollo local

Durante el desarrollo local, la URL de callback de Bluesky se fija en http://127.0.0.1:8000/. Es cómodo redirigir a nivel de ruta.
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());
    }

    // ...
});

Implementación del controlador

<?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)
    {
        // Página del formulario donde se introduce el login hint
        return view('login');
    }

    public function redirect(Request $request)
    {
        // Puedes pasar el handle, DID o URL del PDS como login_hint (opcional)
        $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')) {
            // Depuración durante desarrollo. En producción usa un manejo de errores adecuado
            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;

        // Guarda el OAuthSession en la sesión de 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');
    }
}

Información del usuario (OAuthSession)

Estos son los principales métodos del OAuthSession que puedes obtener desde $user->session.
MétodoDescripciónEjemplo
did()DID de Blueskydid:plc:xxxxxx
handle()Handle de Blueskyalice.bsky.social
displayName()Nombre mostradoAlice
avatar()URL del avatarhttps://...
issuer()URL del PDShttps://bsky.social
token()Token de acceso
refresh()Refresh token
Para ver todas las propiedades, usa toArray().
/** @var OAuthSession $session */
dump($session->toArray());

Configuración de base de datos

Añade a la tabla users las columnas específicas de Bluesky. El DID es el identificador único del usuario en Bluesky.
// 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();
});

Reutilización de OAuthSession

Puedes llamar a las APIs usando el OAuthSession guardado en la sesión.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;

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

$timeline = Bluesky::withToken($session)->getTimeline();
En contextos donde la sesión de Laravel no está disponible, como Jobs o Console, construye el OAuthSession con datos obtenidos de la BD.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;

$session = OAuthSession::create([
    'did'           => $user->did,
    'refresh_token' => $user->refresh_token,
    // Para cuentas que no son de bsky.social, indica también iss
    // 'iss'        => $user->iss,
]);

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

Refresco automático de tokens

Como el refresh token solo se puede usar una vez, tras cada refresco es imprescindible volver a guardarlo en la BD. Se utiliza el evento OAuthSessionUpdated.
php artisan make:listener OAuthSessionUpdatedListener
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();
    }
}
Al iniciar el refresco también se emite el evento OAuthSessionRefreshing. En ese momento el refresh_token ya no es válido, así que es más seguro eliminarlo de la BD.
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();
}

Trait WithBluesky

Si añades el trait WithBluesky al modelo User e implementas tokenForBluesky(), puedes obtener un cliente autenticado con $user->bluesky().
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,
        ]);
    }
}
$profile = auth()->user()
                 ->bluesky()
                 ->refreshSession()
                 ->getProfile();

Personalizar client-metadata

El paquete define automáticamente las rutas bluesky.oauth.client-metadata y bluesky.oauth.jwks. Normalmente no hace falta cambiar nada, pero puedes personalizarlas con OAuthConfig.
// 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();
    });
}

Comportamiento sin autenticación

Si el OAuthSession es null o no hay refresh token, se lanza la excepción Unauthenticated y se redirige a la ruta login.
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->redirectGuestsTo('/bluesky/login');
})
Última modificación el 13 de julio de 2026