Initial commit
This commit is contained in:
@@ -0,0 +1,695 @@
|
||||
# Authentication
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Password authentication, OAuth2 integration, token management, MFA setup, and auth collection configuration.
|
||||
|
||||
---
|
||||
|
||||
## 1. Use Impersonation for Admin Operations
|
||||
|
||||
**Impact: MEDIUM (Safe admin access to user data without password sharing)**
|
||||
|
||||
Impersonation allows superusers to generate tokens for other users, enabling admin support tasks and API key functionality without sharing passwords.
|
||||
|
||||
**Incorrect (sharing credentials or bypassing auth):**
|
||||
|
||||
```javascript
|
||||
// Bad: sharing user passwords for support
|
||||
async function helpUser(userId, userPassword) {
|
||||
await pb.collection('users').authWithPassword(userEmail, userPassword);
|
||||
// Support team knows user's password!
|
||||
}
|
||||
|
||||
// Bad: directly modifying records without proper context
|
||||
async function fixUserData(userId) {
|
||||
// Bypasses user's perspective and rules
|
||||
await pb.collection('posts').update(postId, { fixed: true });
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using impersonation):**
|
||||
|
||||
```javascript
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
// Admin client with superuser auth (use environment variables, never hardcode)
|
||||
const adminPb = new PocketBase(process.env.PB_URL);
|
||||
await adminPb.collection('_superusers').authWithPassword(
|
||||
process.env.PB_SUPERUSER_EMAIL,
|
||||
process.env.PB_SUPERUSER_PASSWORD
|
||||
);
|
||||
|
||||
async function impersonateUser(userId) {
|
||||
// Generate impersonation token (non-renewable)
|
||||
const impersonatedClient = await adminPb
|
||||
.collection('users')
|
||||
.impersonate(userId, 3600); // 1 hour duration
|
||||
|
||||
// impersonatedClient has user's auth context
|
||||
console.log('Acting as:', impersonatedClient.authStore.record.email);
|
||||
|
||||
// Operations use user's permissions
|
||||
const userPosts = await impersonatedClient.collection('posts').getList();
|
||||
|
||||
return impersonatedClient;
|
||||
}
|
||||
|
||||
// Use case: Admin viewing user's data
|
||||
async function adminViewUserPosts(userId) {
|
||||
const userClient = await impersonateUser(userId);
|
||||
|
||||
// See exactly what the user sees (respects API rules)
|
||||
const posts = await userClient.collection('posts').getList();
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
// Use case: API keys for server-to-server communication
|
||||
async function createApiKey(serviceUserId) {
|
||||
// Create a service impersonation token (use short durations, rotate regularly)
|
||||
const serviceClient = await adminPb
|
||||
.collection('service_accounts')
|
||||
.impersonate(serviceUserId, 86400); // 24 hours max, rotate via scheduled task
|
||||
|
||||
// Return token for service to use
|
||||
return serviceClient.authStore.token;
|
||||
}
|
||||
|
||||
// Using API key token in another service
|
||||
async function useApiKey(apiToken) {
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
// Manually set the token
|
||||
pb.authStore.save(apiToken, null);
|
||||
|
||||
// Now requests use the service account's permissions
|
||||
const data = await pb.collection('data').getList();
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
**Important considerations:**
|
||||
|
||||
```javascript
|
||||
// Impersonation tokens are non-renewable
|
||||
const client = await adminPb.collection('users').impersonate(userId, 3600);
|
||||
|
||||
// This will fail - can't refresh impersonation tokens
|
||||
try {
|
||||
await client.collection('users').authRefresh();
|
||||
} catch (error) {
|
||||
// Expected: impersonation tokens can't be refreshed
|
||||
}
|
||||
|
||||
// For continuous access, generate new token when needed
|
||||
async function getImpersonatedClient(userId) {
|
||||
// Check if existing token is still valid
|
||||
if (cachedClient?.authStore.isValid) {
|
||||
return cachedClient;
|
||||
}
|
||||
|
||||
// Generate fresh token
|
||||
return await adminPb.collection('users').impersonate(userId, 3600);
|
||||
}
|
||||
```
|
||||
|
||||
**Security best practices:**
|
||||
- Use short durations for support tasks
|
||||
- Log all impersonation events
|
||||
- Restrict impersonation to specific admin roles
|
||||
- Never expose impersonation capability in client code
|
||||
- Use dedicated service accounts for API keys
|
||||
|
||||
Reference: [PocketBase Impersonation](https://pocketbase.io/docs/authentication/#impersonate-authentication)
|
||||
|
||||
## 2. Implement Multi-Factor Authentication
|
||||
|
||||
**Impact: HIGH (Additional security layer for sensitive applications)**
|
||||
|
||||
MFA requires users to authenticate with two different methods. PocketBase supports OTP (One-Time Password) via email as the second factor.
|
||||
|
||||
**Incorrect (single-factor only for sensitive apps):**
|
||||
|
||||
```javascript
|
||||
// Insufficient for sensitive applications
|
||||
async function login(email, password) {
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
// User immediately has full access - no second factor
|
||||
return authData;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (MFA flow with OTP):**
|
||||
|
||||
```javascript
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
async function loginWithMFA(email, password) {
|
||||
try {
|
||||
// First factor: password
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
// If MFA not required, auth succeeds immediately
|
||||
return { success: true, authData };
|
||||
|
||||
} catch (error) {
|
||||
// MFA required - returns 401 with mfaId
|
||||
if (error.status === 401 && error.response?.mfaId) {
|
||||
return {
|
||||
success: false,
|
||||
mfaRequired: true,
|
||||
mfaId: error.response.mfaId
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOTP(email) {
|
||||
// Request OTP to be sent via email
|
||||
const result = await pb.collection('users').requestOTP(email);
|
||||
|
||||
// Returns otpId - needed to verify the OTP
|
||||
// Note: Returns otpId even if email doesn't exist (prevents enumeration)
|
||||
return result.otpId;
|
||||
}
|
||||
|
||||
async function completeMFAWithOTP(mfaId, otpId, otpCode) {
|
||||
try {
|
||||
// Second factor: OTP verification
|
||||
const authData = await pb.collection('users').authWithOTP(
|
||||
otpId,
|
||||
otpCode,
|
||||
{ mfaId } // Include mfaId from first factor
|
||||
);
|
||||
|
||||
return { success: true, authData };
|
||||
} catch (error) {
|
||||
if (error.status === 400) {
|
||||
throw new Error('Invalid or expired code');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete flow example
|
||||
async function fullMFAFlow(email, password, otpCode = null) {
|
||||
// Step 1: Password authentication
|
||||
const step1 = await loginWithMFA(email, password);
|
||||
|
||||
if (step1.success) {
|
||||
return step1.authData; // MFA not required
|
||||
}
|
||||
|
||||
if (step1.mfaRequired) {
|
||||
// Step 2: Request OTP
|
||||
const otpId = await requestOTP(email);
|
||||
|
||||
// Step 3: UI prompts user for OTP code...
|
||||
// (In real app, wait for user input)
|
||||
|
||||
if (otpCode) {
|
||||
// Step 4: Complete MFA
|
||||
const step2 = await completeMFAWithOTP(step1.mfaId, otpId, otpCode);
|
||||
return step2.authData;
|
||||
}
|
||||
|
||||
return { pendingMFA: true, mfaId: step1.mfaId, otpId };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configure MFA (Admin UI or API):**
|
||||
|
||||
```javascript
|
||||
// Enable MFA on auth collection (superuser only)
|
||||
await pb.collections.update('users', {
|
||||
mfa: {
|
||||
enabled: true,
|
||||
duration: 1800, // MFA session duration (30 min)
|
||||
rule: '' // When to require MFA (empty = always for all users)
|
||||
// rule: '@request.auth.role = "admin"' // Only for admins
|
||||
},
|
||||
otp: {
|
||||
enabled: true,
|
||||
duration: 300, // OTP validity (5 min)
|
||||
length: 6, // OTP code length
|
||||
emailTemplate: {
|
||||
subject: 'Your verification code',
|
||||
body: 'Your code is: {OTP}'
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**MFA best practices:**
|
||||
- Always enable for admin accounts
|
||||
- Consider making MFA optional for regular users
|
||||
- Use short OTP durations (5-10 minutes)
|
||||
- Implement rate limiting on OTP requests
|
||||
- Log MFA events for security auditing
|
||||
|
||||
Reference: [PocketBase MFA](https://pocketbase.io/docs/authentication/#mfa)
|
||||
|
||||
## 3. Integrate OAuth2 Providers Correctly
|
||||
|
||||
**Impact: CRITICAL (Secure third-party authentication with proper flow handling)**
|
||||
|
||||
OAuth2 integration should use the all-in-one method for simplicity and security. Manual code exchange should only be used when necessary (e.g., mobile apps with deep links).
|
||||
|
||||
**Incorrect (manual implementation without SDK):**
|
||||
|
||||
```javascript
|
||||
// Don't manually handle OAuth flow
|
||||
async function loginWithGoogle() {
|
||||
// Redirect user to Google manually
|
||||
window.location.href = 'https://accounts.google.com/oauth/authorize?...';
|
||||
}
|
||||
|
||||
// Manual callback handling
|
||||
async function handleCallback(code) {
|
||||
// Exchange code manually - error prone!
|
||||
const response = await fetch('/api/auth/callback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using SDK's all-in-one method):**
|
||||
|
||||
```javascript
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
// All-in-one OAuth2 (recommended for web apps)
|
||||
async function loginWithOAuth2(providerName) {
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({
|
||||
provider: providerName, // 'google', 'github', 'microsoft', etc.
|
||||
// Optional: create new user data if not exists
|
||||
createData: {
|
||||
emailVisibility: true,
|
||||
name: '' // Will be populated from OAuth provider
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Logged in via', providerName);
|
||||
console.log('User:', authData.record.email);
|
||||
console.log('Is new user:', authData.meta?.isNew);
|
||||
|
||||
return authData;
|
||||
} catch (error) {
|
||||
if (error.isAbort) {
|
||||
console.log('OAuth popup was closed');
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
document.getElementById('google-btn').onclick = () => loginWithOAuth2('google');
|
||||
document.getElementById('github-btn').onclick = () => loginWithOAuth2('github');
|
||||
```
|
||||
|
||||
**Manual code exchange (for React Native / deep links):**
|
||||
|
||||
```javascript
|
||||
// Only use when all-in-one isn't possible
|
||||
async function loginWithOAuth2Manual() {
|
||||
// Get auth methods - PocketBase provides state and codeVerifier
|
||||
const authMethods = await pb.collection('users').listAuthMethods();
|
||||
const provider = authMethods.oauth2.providers.find(p => p.name === 'google');
|
||||
|
||||
// Store the provider's state and codeVerifier for callback verification
|
||||
// PocketBase generates these for you - don't create your own
|
||||
sessionStorage.setItem('oauth_state', provider.state);
|
||||
sessionStorage.setItem('oauth_code_verifier', provider.codeVerifier);
|
||||
|
||||
// Build the OAuth URL using provider.authURL + redirect
|
||||
const redirectUrl = window.location.origin + '/oauth-callback';
|
||||
const authUrl = provider.authURL + encodeURIComponent(redirectUrl);
|
||||
|
||||
// Redirect to OAuth provider
|
||||
window.location.href = authUrl;
|
||||
}
|
||||
|
||||
// In your callback handler (e.g., /oauth-callback page):
|
||||
async function handleOAuth2Callback() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// CSRF protection: verify state matches
|
||||
if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
|
||||
throw new Error('State mismatch - potential CSRF attack');
|
||||
}
|
||||
|
||||
const code = params.get('code');
|
||||
const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
|
||||
const redirectUrl = window.location.origin + '/oauth-callback';
|
||||
|
||||
// Exchange code for auth token
|
||||
const authData = await pb.collection('users').authWithOAuth2Code(
|
||||
'google',
|
||||
code,
|
||||
codeVerifier,
|
||||
redirectUrl,
|
||||
{ emailVisibility: true }
|
||||
);
|
||||
|
||||
// Clean up
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
sessionStorage.removeItem('oauth_code_verifier');
|
||||
|
||||
return authData;
|
||||
}
|
||||
```
|
||||
|
||||
**Configure OAuth2 provider (Admin UI or API):**
|
||||
|
||||
```javascript
|
||||
// Via API (superuser only) - usually done in Admin UI
|
||||
// IMPORTANT: Never hardcode client secrets. Use environment variables.
|
||||
await pb.collections.update('users', {
|
||||
oauth2: {
|
||||
enabled: true,
|
||||
providers: [{
|
||||
name: 'google',
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET
|
||||
}],
|
||||
mappedFields: {
|
||||
avatarURL: 'avatar' // Map OAuth field to collection field
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Reference: [PocketBase OAuth2](https://pocketbase.io/docs/authentication/#oauth2-authentication)
|
||||
|
||||
## 4. Use authWithOTP for Email One-Time Codes, Rate-Limit requestOTP
|
||||
|
||||
**Impact: HIGH (OTP endpoints are unauthenticated; unthrottled requestOTP enables email bombing and enumeration)**
|
||||
|
||||
Auth collections can enable **OTP login** from the admin UI (Collection → Options → "Enable OTP"). The client flow is two steps: `requestOTP(email)` returns an `otpId`, then `authWithOTP(otpId, code)` exchanges the id + code for an auth token. Two things trip people up: (1) the OTP response is **the same whether the email exists or not** - do not break that by leaking a distinct error; (2) `requestOTP` sends an email, so **it must be rate-limited** or an attacker can use it to spam any address.
|
||||
|
||||
**Incorrect (leaks existence, custom requestOTP with no rate limit):**
|
||||
|
||||
```javascript
|
||||
// ❌ Client-side existence check - ignore the 404 and expose it to the user
|
||||
try {
|
||||
await pb.collection("users").getFirstListItem(`email="${email}"`);
|
||||
} catch (e) {
|
||||
alert("No account with that email"); // ❌ account enumeration
|
||||
return;
|
||||
}
|
||||
|
||||
// ❌ Ad-hoc route with no rate limit - attacker hammers this to spam mailboxes
|
||||
routerAdd("POST", "/api/myapp/otp", (e) => {
|
||||
const body = e.requestInfo().body;
|
||||
const user = $app.findAuthRecordByEmail("users", body.email);
|
||||
// send custom email...
|
||||
return e.json(200, { ok: true });
|
||||
});
|
||||
```
|
||||
|
||||
**Correct (use the built-in flow, step 1 always returns an otpId):**
|
||||
|
||||
```javascript
|
||||
// Step 1: request the code. Always returns { otpId } - even if the email
|
||||
// does not exist, PocketBase returns a synthetic id so enumeration is
|
||||
// impossible. Treat every response as success from the UI perspective.
|
||||
const { otpId } = await pb.collection("users").requestOTP("user@example.com");
|
||||
|
||||
// Step 2: exchange otpId + the 8-digit code the user typed
|
||||
const authData = await pb.collection("users").authWithOTP(
|
||||
otpId,
|
||||
"12345678",
|
||||
);
|
||||
// pb.authStore is now populated
|
||||
```
|
||||
|
||||
```go
|
||||
// Go side - rate-limit and log if you wrap your own endpoint
|
||||
app.OnRecordRequestOTPRequest("users").BindFunc(func(e *core.RecordRequestOTPRequestEvent) error {
|
||||
// e.Collection, e.Record (may be nil - synthetic id path),
|
||||
// e.Email (always present), e.Password (unused for OTP)
|
||||
e.App.Logger().Info("otp requested",
|
||||
"email", e.Email,
|
||||
"ip", e.RequestInfo.Headers["x_forwarded_for"])
|
||||
return e.Next() // REQUIRED
|
||||
})
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `requestOTP` **always returns 200 with an otpId**, even for non-existent emails - preserve that by never adding a pre-check or a different error path.
|
||||
- Enable the built-in rate limiter (see `deploy-rate-limiting.md`) and raise the cost for the `*:requestOTP` label. Without this, an attacker can email-bomb arbitrary users.
|
||||
- The OTP code is 8 digits by default, with a 3-minute TTL. Do not extend the TTL - short windows are the whole point.
|
||||
- `authWithOTP` consumes the code; a successful call invalidates the `otpId`. Always show a generic "invalid or expired code" on failure.
|
||||
- If you want OTP **without a password**, set the collection's `Password` option to off and `OTP` on. If both are enabled, users can use either.
|
||||
- OTP emails are sent via the configured SMTP server. In dev, point SMTP at Mailpit or a console logger before testing - do **not** ship with the default "no-reply@example.com" sender.
|
||||
|
||||
Reference: [Auth with OTP](https://pocketbase.io/docs/authentication/#auth-with-otp) · [JS SDK - authWithOTP](https://github.com/pocketbase/js-sdk#authwithotp)
|
||||
|
||||
## 5. Implement Secure Password Authentication
|
||||
|
||||
**Impact: CRITICAL (Secure user login with proper error handling and token management)**
|
||||
|
||||
Password authentication should include proper error handling, avoid exposing whether emails exist, and correctly manage the auth store.
|
||||
|
||||
**Incorrect (exposing information and poor error handling):**
|
||||
|
||||
```javascript
|
||||
// Dangerous: exposes whether email exists
|
||||
async function login(email, password) {
|
||||
const user = await pb.collection('users').getFirstListItem(`email = "${email}"`);
|
||||
if (!user) {
|
||||
throw new Error('Email not found'); // Reveals email doesn't exist
|
||||
}
|
||||
|
||||
// Manual password check - never do this!
|
||||
if (user.password !== password) {
|
||||
throw new Error('Wrong password'); // Reveals password is wrong
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (secure authentication):**
|
||||
|
||||
```javascript
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
async function login(email, password) {
|
||||
try {
|
||||
// authWithPassword handles hashing and returns token
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
// Token is automatically stored in pb.authStore
|
||||
console.log('Logged in as:', authData.record.email);
|
||||
console.log('Token valid:', pb.authStore.isValid);
|
||||
|
||||
return authData;
|
||||
} catch (error) {
|
||||
// Generic error message - don't reveal if email exists
|
||||
if (error.status === 400) {
|
||||
throw new Error('Invalid email or password');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
function isAuthenticated() {
|
||||
return pb.authStore.isValid;
|
||||
}
|
||||
|
||||
// Get current user
|
||||
function getCurrentUser() {
|
||||
return pb.authStore.record;
|
||||
}
|
||||
|
||||
// Logout
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
}
|
||||
|
||||
// Listen for auth changes
|
||||
pb.authStore.onChange((token, record) => {
|
||||
console.log('Auth state changed:', record?.email || 'logged out');
|
||||
}, true); // true = fire immediately with current state
|
||||
```
|
||||
|
||||
**Auth collection configuration for password auth:**
|
||||
|
||||
```javascript
|
||||
// When creating auth collection via API (superuser only)
|
||||
await pb.collections.create({
|
||||
name: 'users',
|
||||
type: 'auth',
|
||||
fields: [
|
||||
{ name: 'name', type: 'text' },
|
||||
{ name: 'avatar', type: 'file', options: { maxSelect: 1 } }
|
||||
],
|
||||
passwordAuth: {
|
||||
enabled: true,
|
||||
identityFields: ['email', 'username'] // Fields that can be used to login
|
||||
},
|
||||
// Require minimum password length
|
||||
// (configured in Admin UI under collection options)
|
||||
});
|
||||
```
|
||||
|
||||
**Security considerations:**
|
||||
- Never store passwords in plain text
|
||||
- Use generic error messages
|
||||
- Implement rate limiting on your server
|
||||
- Consider adding MFA for sensitive applications
|
||||
|
||||
Reference: [PocketBase Auth](https://pocketbase.io/docs/authentication/)
|
||||
|
||||
## 6. Manage Auth Tokens Properly
|
||||
|
||||
**Impact: CRITICAL (Prevents unauthorized access, handles token expiration gracefully)**
|
||||
|
||||
Auth tokens should be refreshed before expiration, validated on critical operations, and properly cleared on logout. The SDK's authStore handles most of this automatically.
|
||||
|
||||
**Incorrect (ignoring token expiration):**
|
||||
|
||||
```javascript
|
||||
// Bad: never checking token validity
|
||||
async function fetchUserData() {
|
||||
// Token might be expired!
|
||||
const records = await pb.collection('posts').getList();
|
||||
return records;
|
||||
}
|
||||
|
||||
// Bad: manually managing tokens
|
||||
let authToken = localStorage.getItem('token');
|
||||
fetch('/api/posts', {
|
||||
headers: { 'Authorization': authToken } // Token might be invalid
|
||||
});
|
||||
```
|
||||
|
||||
**Correct (proper token management):**
|
||||
|
||||
```javascript
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
// Check token validity before operations
|
||||
async function fetchSecureData() {
|
||||
// authStore.isValid is a client-side check only (JWT expiry parsing).
|
||||
// Always verify server-side with authRefresh() for critical operations.
|
||||
if (!pb.authStore.isValid) {
|
||||
throw new Error('Please log in');
|
||||
}
|
||||
|
||||
return pb.collection('posts').getList();
|
||||
}
|
||||
|
||||
// Refresh token periodically or before expiration
|
||||
async function refreshAuthIfNeeded() {
|
||||
if (!pb.authStore.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verifies current token and returns fresh one
|
||||
await pb.collection('users').authRefresh();
|
||||
console.log('Token refreshed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Token invalid - user needs to re-authenticate
|
||||
pb.authStore.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh on app initialization
|
||||
async function initializeAuth() {
|
||||
if (pb.authStore.token) {
|
||||
try {
|
||||
await pb.collection('users').authRefresh();
|
||||
} catch {
|
||||
pb.authStore.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for auth changes and handle expiration
|
||||
pb.authStore.onChange((token, record) => {
|
||||
if (!token) {
|
||||
// User logged out or token cleared
|
||||
redirectToLogin();
|
||||
}
|
||||
});
|
||||
|
||||
// Setup periodic refresh (e.g., every 10 minutes)
|
||||
setInterval(async () => {
|
||||
if (pb.authStore.isValid) {
|
||||
try {
|
||||
await pb.collection('users').authRefresh();
|
||||
} catch {
|
||||
pb.authStore.clear();
|
||||
}
|
||||
}
|
||||
}, 10 * 60 * 1000);
|
||||
```
|
||||
|
||||
**SSR / Server-side token handling:**
|
||||
|
||||
```javascript
|
||||
// Server-side: create fresh client per request
|
||||
export async function handleRequest(request) {
|
||||
const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
|
||||
// Load auth from cookie
|
||||
pb.authStore.loadFromCookie(request.headers.get('cookie') || '');
|
||||
|
||||
// Validate and refresh
|
||||
if (pb.authStore.isValid) {
|
||||
try {
|
||||
await pb.collection('users').authRefresh();
|
||||
} catch {
|
||||
pb.authStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ... handle request ...
|
||||
|
||||
// Send updated cookie with secure options
|
||||
const response = new Response();
|
||||
response.headers.set('set-cookie', pb.authStore.exportToCookie({
|
||||
httpOnly: true, // Prevent XSS access to auth token
|
||||
secure: true, // HTTPS only
|
||||
sameSite: 'Lax', // CSRF protection
|
||||
}));
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
**Token configuration (Admin UI or migration):**
|
||||
|
||||
```javascript
|
||||
// Configure token durations (superuser only)
|
||||
await pb.collections.update('users', {
|
||||
authToken: {
|
||||
duration: 1209600 // 14 days in seconds
|
||||
},
|
||||
verificationToken: {
|
||||
duration: 604800 // 7 days
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Reference: [PocketBase Auth Store](https://pocketbase.io/docs/authentication/)
|
||||
|
||||
Reference in New Issue
Block a user