3.9 KiB
3.9 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Authenticate Realtime Connections | MEDIUM | Secure subscriptions respecting API rules | realtime, authentication, security, subscriptions |
Authenticate Realtime Connections
Realtime subscriptions respect collection API rules. Ensure the connection is authenticated before subscribing to protected data.
Incorrect (subscribing without auth context):
// Subscribing before authentication
const pb = new PocketBase('http://127.0.0.1:8090');
// This will fail or return no data if collection requires auth
pb.collection('private_messages').subscribe('*', (e) => {
// Won't receive events - not authenticated!
console.log(e.record);
});
// Later user logs in, but subscription doesn't update
await pb.collection('users').authWithPassword(email, password);
// Existing subscription still unauthenticated!
Correct (authenticated subscriptions):
// Subscribe after authentication
const pb = new PocketBase('http://127.0.0.1:8090');
async function initRealtime() {
// First authenticate
await pb.collection('users').authWithPassword(email, password);
// Now subscribe - will use auth context
pb.collection('private_messages').subscribe('*', (e) => {
// Receives events for messages user can access
console.log('New message:', e.record);
});
}
// Re-subscribe after auth changes
function useAuthenticatedRealtime() {
const [messages, setMessages] = useState([]);
const unsubRef = useRef(null);
// Watch auth changes
useEffect(() => {
const removeListener = pb.authStore.onChange((token, record) => {
// Unsubscribe old connection
if (unsubRef.current) {
unsubRef.current();
unsubRef.current = null;
}
// Re-subscribe with new auth context if logged in
if (record) {
setupSubscription();
} else {
setMessages([]);
}
}, true);
return () => {
removeListener();
if (unsubRef.current) unsubRef.current();
};
}, []);
async function setupSubscription() {
unsubRef.current = await pb.collection('private_messages').subscribe('*', (e) => {
handleMessage(e);
});
}
}
// Handle auth token refresh with realtime
pb.realtime.subscribe('PB_CONNECT', async (e) => {
console.log('Realtime connected');
// Verify auth is still valid
if (pb.authStore.isValid) {
try {
await pb.collection('users').authRefresh();
} catch {
pb.authStore.clear();
// Redirect to login
}
}
});
API rules apply to subscriptions:
// Collection rule: listRule: 'owner = @request.auth.id'
// User A subscribed
await pb.collection('users').authWithPassword('a@test.com', 'password');
pb.collection('notes').subscribe('*', handler);
// Only receives events for notes where owner = User A
// Events from other users' notes are filtered out automatically
Subscription authorization flow:
- SSE connection established (no auth check)
- First subscription triggers authorization
- Auth token from
pb.authStoreis used - Collection rules evaluated for each event
- Only matching events sent to client
Handling auth expiration:
// Setup disconnect handler
pb.realtime.onDisconnect = (subscriptions) => {
console.log('Disconnected, had subscriptions:', subscriptions);
// Check if auth expired
if (!pb.authStore.isValid) {
// Token expired - need to re-authenticate
redirectToLogin();
return;
}
// Connection issue - realtime will auto-reconnect
// Re-subscribe after reconnection
pb.realtime.subscribe('PB_CONNECT', () => {
resubscribeAll(subscriptions);
});
};
function resubscribeAll(subscriptions) {
subscriptions.forEach(sub => {
const [collection, topic] = sub.split('/');
pb.collection(collection).subscribe(topic, handlers[sub]);
});
}
Reference: PocketBase Realtime Auth