atproto_oauth 0.5.0
atproto_oauth: ^0.5.0 copied to clipboard
Provides tools to handle OAuth for AT Protocol and Bluesky Social.
AT Protocol OAuth Authentication for Flutter #
This guide explains how to implement AT Protocol OAuth authentication in your Flutter application using FlutterWebAuth2 for services like Bluesky.
Client Metadata #
See [AT Protocol instruction] about client metadata.
Installation #
Add the following dependencies to your pubspec.yaml:
dependencies:
atproto_oauth: ^0.5.0 # Replace with actual version
flutter_web_auth_2: ^4.0.1
flutter_secure_storage: ^9.2.2
Or if you would like to use this feature on Bluesky:
dependencies:
bluesky: ^2.0.0 # Replace with actual version
flutter_web_auth_2: ^4.0.1
flutter_secure_storage: ^9.2.2
Note: AT Protocol OAuth tokens are opaque — this library never decodes them as JWTs. Every stage of the flow is pluggable: inject your own
OAuthStateStore,OAuthSessionStore,DPoPNonceCache,IdentityResolver, orDPoPSignerintoOAuthClient(each has an in-memory / HTTP / pointycastle default). The example below manages session persistence explicitly instead.
Basic Usage #
Here's how to implement AT Protocol OAuth authentication in your Flutter app:
import 'dart:convert';
import 'package:atproto_oauth/atproto_oauth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class BlueskyAuth extends StatefulWidget {
@override
_BlueskyAuthState createState() => _BlueskyAuthState();
}
class _BlueskyAuthState extends State<BlueskyAuth> {
late OAuthClient _client;
final _storage = const FlutterSecureStorage();
@override
void initState() {
super.initState();
_initializeOAuth();
}
Future<void> _initializeOAuth() async {
// Initialize OAuth client with metadata
// Replace with your client metadata
final metadata = await getClientMetadata(
'https://atprotodart.com/oauth/bluesky/atprotodart/client-metadata.json'
);
_client = OAuthClient(metadata);
}
Future<void> _startAuth() async {
try {
// Resolve the account and get the authorization URL. The per-request
// context is stored inside the client's OAuthStateStore (keyed by the
// `state` value), so `callback` below does not need it passed back.
final authUrl = await _client.authorize('shinyakato.dev');
// Launch OAuth flow in browser
final result = await FlutterWebAuth2.authenticate(
url: authUrl.toString(),
callbackUrlScheme: 'your-app-scheme',
);
// Handle the OAuth callback (single argument)
final session = await _client.callback(result);
// Store the session securely
await _saveSession(session);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Successfully logged in!')),
);
} catch (e) {
// Handle errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Authentication failed: $e')),
);
}
}
Future<void> _saveSession(OAuthSession session) async {
// OAuthSession serializes to/from JSON. It contains the DPoP private key
// and the access/refresh tokens, so only persist it into secure,
// access-controlled storage — never plaintext.
await _storage.write(
key: 'oauth_session',
value: jsonEncode(session.toJson()),
);
}
Future<OAuthSession?> _loadSession() async {
final raw = await _storage.read(key: 'oauth_session');
if (raw == null) return null;
// Sessions saved by atproto_oauth <= 0.4.x used a different shape; restore
// those with `OAuthSession.fromLegacyJson(json, issuer: ..., pds: ...)`.
return OAuthSession.fromJson(jsonDecode(raw) as Map<String, dynamic>);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _startAuth,
child: Text('Login with Bluesky'),
),
),
);
}
}
Platform Configuration #
See docs on flutter_web_auth_2.
Using Bluesky Client #
Once authenticated, wrap the session in an OAuthSessionManager and hand it to
the bluesky client. The manager builds the
DPoP proof for every request and refreshes the access token automatically —
proactively just before it expires and reactively on a 401 — so you no longer
refresh by hand. Listen to onSessionUpdated to persist the rotated session.
Future<Bluesky?> _authenticatedBluesky() async {
final session = await _loadSession();
if (session == null) {
// Handle unauthenticated state
return null;
}
// Pass the client so the manager can refresh; persist rotations.
final manager = OAuthSessionManager.fromSession(session, client: _client);
manager.onSessionUpdated.listen(_saveSession);
return Bluesky.fromOAuth(manager);
}
Future<void> _makeAuthenticatedRequest() async {
final bsky = await _authenticatedBluesky();
if (bsky == null) return;
// Anyway you want it !
final record = await bsky.feed.post.create(text: 'Nice DPoP proof');
}
If a refresh fails because the session was revoked, the manager surfaces an
OAuthSessionRevokedExceptionand clears it from the session store — route the user back throughauthorizein that case.