Überblick
Blueskys OAuth basiert auf dem AT Protocol und unterscheidet sich deutlich von gewöhnlichen Socialite-Providern wie GitHub oder Google.
Blueskys OAuth ist in seiner Implementierung grundsätzlich anders als andere Socialite-Provider. Es verwendet DPoP (Demonstrated Proof of Possession) und den PAR-Endpunkt (Pushed Authorization Requests). Ein client_secret wird nicht benötigt, stattdessen kommt ein privater Schlüssel zum Einsatz.
Unterschied zum normalen OAuth
| Punkt | Übliches Socialite | Bluesky Socialite |
|---|
| Authentifizierungsverfahren | OAuth 2.0 | AT-Protocol-OAuth (DPoP) |
| Client-Identifikation | client_id + client_secret | Privater Schlüssel + URL zu client-metadata.json |
client_id | Registrierter fester String | URL zur client-metadata.json |
| Nutzeridentifier | E-Mail-Adresse etc. | DID (did:plc:...) oder Handle |
| Login-Hint | Nicht erforderlich | Handle / DID / PDS-URL übergebbar |
Authentifizierungsfluss
Installation und Konfiguration
Privaten Schlüssel erstellen
Erzeugen Sie zunächst einen privaten Schlüssel. Das ist ohne Registrierung bei Bluesky möglich.
php artisan bluesky:new-private-key
Please set this private key in .env
BLUESKY_OAUTH_PRIVATE_KEY="...url-safe base64 encoded key..."
Kopieren Sie den ausgegebenen Wert in .env.
BLUESKY_OAUTH_PRIVATE_KEY="..."
Bei Bluesky ist keine Registrierung eines client_id oder client_secret erforderlich. Allein mit dem privaten Schlüssel können Sie die OAuth-Authentifizierung nutzen.
Standard-OAuth-Scopes
Das Paket ist mit Standard-OAuth-Scopes vorkonfiguriert, die drei Hauptanwendungsfälle abdecken.
- Socialite-Login – Aktiviert Nutzer-Authentifizierung und E-Mail-Zugriff über
atproto, account:email, include:app.bsky.authViewAll.
- Posten – Erlaubt das Erstellen von Posts und das Hochladen von Bildern/Videos über
include:app.bsky.authCreatePosts und blob:*/*.
- DM-Benachrichtigungen – Aktiviert das Versenden von DMs für Benachrichtigungen über
rpc:chat.bsky.convo.sendMessage und rpc:chat.bsky.convo.getConvoForMembers.
Sie können die Scopes über die Umgebungsvariable BLUESKY_OAUTH_SCOPE anpassen.
BLUESKY_OAUTH_SCOPE="atproto account:email include:app.bsky.authViewAll"
Details zu den verfügbaren Scopes finden Sie in der AT-Protocol-Dokumentation zu Permission Requests.
Lokale Entwicklung
Standardmäßig sind http://localhost und http://127.0.0.1:8000/ konfiguriert, sodass für die lokale Entwicklung keine zusätzliche Konfiguration nötig ist.
# Nur setzen, wenn Sie den Port ändern
BLUESKY_CLIENT_ID=
BLUESKY_REDIRECT=http://127.0.0.1:8080/
Produktivumgebung
Wenn der Routenname bluesky.oauth.redirect existiert, ist kein Eintrag in .env nötig. Wenn Sie den Standard-Routennamen ändern, konfigurieren Sie ihn.
BLUESKY_REDIRECT=/bluesky/callback
Routen-Setup
Für die Callback-Route wird der Routenname bluesky.oauth.redirect empfohlen. Das Paket verwendet diesen Namen intern.
// 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');
Callback-Handling in der lokalen Entwicklung
Während der lokalen Entwicklung ist die Callback-URL von Bluesky fest auf http://127.0.0.1:8000/ gesetzt. Es ist praktisch, das Routing dafür auf Route-Ebene vorzunehmen.
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());
}
// ...
});
Controller-Implementierung
<?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)
{
// Formular-Seite zur Eingabe eines Login-Hints
return view('login');
}
public function redirect(Request $request)
{
// Handle, DID oder PDS-URL kann als login_hint übergeben werden (optional)
$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')) {
// Zum Debuggen während der Entwicklung. In Produktion durch angemessene Fehlerbehandlung ersetzen.
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 in der Laravel-Session ablegen
$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');
}
}
Die wichtigsten Methoden der über $user->session verfügbaren OAuthSession:
| Methode | Beschreibung | Beispiel |
|---|
did() | Bluesky-DID | did:plc:xxxxxx |
handle() | Bluesky-Handle | alice.bsky.social |
displayName() | Anzeigename | Alice |
avatar() | Avatar-URL | https://... |
issuer() | URL des PDS | https://bsky.social |
token() | Access Token | |
refresh() | Refresh Token | |
Alle Eigenschaften prüfen Sie mit toArray().
/** @var OAuthSession $session */
dump($session->toArray());
Datenbank-Konfiguration
Fügen Sie der Tabelle users Bluesky-spezifische Spalten hinzu. Die DID ist der eindeutige Identifier eines Bluesky-Nutzers.
// 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();
});
Wiederverwendung der OAuthSession
Mit der in der Session gespeicherten OAuthSession können Sie APIs aufrufen.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;
$session = OAuthSession::create(session('bluesky_session'));
$timeline = Bluesky::withToken($session)->getTimeline();
Wenn Sie in Jobs oder in der Konsole keine Laravel-Session verwenden können, bauen Sie die OAuthSession aus den DB-Werten zusammen.
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Session\OAuthSession;
$session = OAuthSession::create([
'did' => $user->did,
'refresh_token' => $user->refresh_token,
// Für Accounts außerhalb von bsky.social auch iss angeben
// 'iss' => $user->iss,
]);
$timeline = Bluesky::withToken($session)
->refreshSession()
->getTimeline();
Automatisches Token-Refreshing
Da das Refresh-Token nur einmal verwendet werden kann, muss es nach dem Aktualisieren unbedingt erneut in der DB gespeichert werden. Verwenden Sie hierfür das Event 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();
}
}
Beim Beginn eines Refreshs wird zudem das Event OAuthSessionRefreshing ausgelöst. Da das refresh_token ab diesem Zeitpunkt ungültig ist, sollten Sie es sicherheitshalber aus der DB entfernen.
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
Wenn Sie dem User-Modell den Trait WithBluesky hinzufügen und tokenForBluesky() implementieren, können Sie über $user->bluesky() einen authentifizierten Client abrufen.
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();
Das Paket definiert automatisch die Routen bluesky.oauth.client-metadata und bluesky.oauth.jwks. Änderungen sind meist nicht nötig, mit OAuthConfig können Sie sie aber anpassen.
// 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();
});
}
Verhalten ohne Authentifizierung
Wenn OAuthSession null ist oder kein Refresh-Token vorliegt, wird eine Unauthenticated-Exception ausgelöst und ein Redirect zur login-Route ausgeführt.
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->redirectGuestsTo('/bluesky/login');
})
Zuletzt geändert am 13. Juli 2026