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
| Elemento | Socialite convencional | Bluesky Socialite |
|---|
| Método de autenticación | OAuth 2.0 | AT Protocol OAuth (DPoP) |
| Identificación del cliente | client_id + client_secret | Clave privada + URL de client-metadata.json |
client_id | Cadena fija registrada | URL de client-metadata.json |
| Identificador de usuario | Correo electrónico, etc. | DID (did:plc:...) o handle |
| Login hint | No necesario | Se 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.
- Login con Socialite — con
atproto, account:email e include:app.bsky.authViewAll se habilita la autenticación del usuario y el acceso al correo.
- Publicaciones — con
include:app.bsky.authCreatePosts y blob:*/* se permite crear publicaciones y subir imágenes/vídeos.
- 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');
}
}
Estos son los principales métodos del OAuthSession que puedes obtener desde $user->session.
| Método | Descripción | Ejemplo |
|---|
did() | DID de Bluesky | did:plc:xxxxxx |
handle() | Handle de Bluesky | alice.bsky.social |
displayName() | Nombre mostrado | Alice |
avatar() | URL del avatar | https://... |
issuer() | URL del PDS | https://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();
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');
})