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.0.1 # 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: ^0.18.0 # Replace with actual version
flutter_web_auth_2: ^4.0.1
flutter_secure_storage: ^9.2.2
Basic Usage
Here's how to implement AT Protocol OAuth authentication in your Flutter app:
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 {
// Get authorization URL for user's handle
final (authUrl, ctx) = await _client.authorize('shinyakato.dev');
// Launch OAuth flow in browser
final result = await FlutterWebAuth2.authenticate(
url: authUrl,
callbackUrlScheme: 'your-app-scheme',
);
// Handle the OAuth callback
final session = await _client.callback(result, ctx);
// 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 {
// Securely store all session data
await _storage.write(key: 'access_token', value: session.accessToken);
await _storage.write(key: 'refresh_token', value: session.refreshToken);
await _storage.write(key: 'dpop_nonce', value: session.$dPoPNonce);
await _storage.write(key: 'public_key', value: session.$publicKey);
await _storage.write(key: 'private_key', value: session.$privateKey);
await _storage.write(
key: 'expires_at',
value: session.expiresAt.toIso8601String(),
);
}
Future<OAuthSession?> _loadSession() async {
final accessToken = await _storage.read(key: 'access_token');
if (accessToken == null) return null;
return OAuthSession(
accessToken: accessToken,
refreshToken: await _storage.read(key: 'refresh_token') ?? '',
tokenType: 'DPoP',
expiresAt: DateTime.parse(
await _storage.read(key: 'expires_at') ?? '',
),
$dPoPNonce: await _storage.read(key: 'dpop_nonce') ?? '',
$publicKey: await _storage.read(key: 'public_key') ?? '',
$privateKey: await _storage.read(key: 'private_key') ?? '',
);
}
Future<OAuthSession?> _refreshTokenIfNeeded() async {
final session = await _loadSession();
if (session == null) return null;
// Check if token needs refresh (e.g., 5 minutes before expiration)
if (session.expiresAt.isBefore(DateTime.now().add(Duration(minutes: 5)))) {
try {
final newSession = await _client.refresh(session);
await _saveSession(newSession);
return newSession;
} catch (e) {
// If refresh fails, clear stored session
await _storage.deleteAll();
return null;
}
}
return session;
}
@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, you can use the session for API requests with bluesky client
Future<void> _makeAuthenticatedRequest() async {
final session = await _refreshTokenIfNeeded();
if (session == null) {
// Handle unauthenticated state
return;
}
final bsky = Bluesky.fromOAuthSession(session);
// Anyway you want it !
final record = await bsky.feed.post(text: 'Nice DPoP proof');
}
License
This project is licensed under the MIT License - see the LICENSE file for details.