Security Best Practices
Learn how to implement "Login with Flowsta" securely and protect your users.
Overview
OAuth 2.0 is secure when implemented correctly. This guide covers best practices, common pitfalls, and security patterns for Login with Flowsta.
Critical Security Measures
1. Always Use PKCE
Proof Key for Code Exchange (PKCE) prevents authorization code interception attacks.
Required for Public Clients
If your application runs in the browser (SPA) or on mobile devices, PKCE is mandatory. Without it, attackers can steal authorization codes.
How PKCE Works:
// 1. Generate random code verifier
const codeVerifier = crypto.randomBytes(32).toString('base64url');
// 2. Create SHA256 hash (code challenge)
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// 3. Store verifier securely
sessionStorage.setItem('code_verifier', codeVerifier);
// 4. Send challenge in authorization request
const authUrl = `https://auth-api.flowsta.com/oauth/authorize?
code_challenge=${codeChallenge}&
code_challenge_method=S256&
...other params`;
// 5. Send verifier in token exchange (backend)
POST /oauth/token
{
"code": "...",
"code_verifier": codeVerifier
}Automatic PKCE
The @flowsta/login-button package handles PKCE automatically. You don't need to implement it yourself.
2. Implement CSRF Protection with State
The state parameter prevents Cross-Site Request Forgery (CSRF) attacks.
Implementation:
// Frontend: Generate and store state
const state = crypto.randomBytes(16).toString('hex');
sessionStorage.setItem('oauth_state', state);
// Include in authorization request
const authUrl = `https://auth-api.flowsta.com/oauth/authorize?
state=${state}&
...other params`;
// Backend: Verify state in callback
app.get('/auth/callback', (req, res) => {
const { state, code } = req.query;
const storedState = req.session.oauthState;
if (!state || state !== storedState) {
return res.status(403).send('Possible CSRF attack detected');
}
// Continue with token exchange...
});Always Verify State
Never skip state verification. An attacker can trick a user into authorizing an app under the attacker's control.
3. Use HTTPS Everywhere
All redirect URIs must use HTTPS in production (except localhost for development).
✅ Correct:
// Production
redirectUri: 'https://yourapp.com/auth/callback'
// Development (localhost exception)
redirectUri: 'http://localhost:3000/auth/callback'❌ Wrong:
// ⚠️ Insecure in production
redirectUri: 'http://yourapp.com/auth/callback'5. Validate Redirect URIs
Always configure exact redirect URIs in your app settings. Wildcard URIs are dangerous.
✅ Correct:
https://yourapp.com/auth/callback
https://app.yourapp.com/callback
http://localhost:3000/callback❌ Wrong:
https://*.yourapp.com/callback ⚠️ Wildcard allows subdomains
https://yourapp.com/* ⚠️ Allows any path6. Store Tokens Securely
Backend (Node.js/Express):
// ✅ Secure: HTTP-only cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000, // 1 hour
});
// ✅ Secure: Server-side session
req.session.accessToken = accessToken;Frontend (SPA):
// ⚠️ Less secure but sometimes necessary
// Use sessionStorage (clears on tab close)
sessionStorage.setItem('access_token', accessToken);
// ❌ Never use localStorage for long-term storage
localStorage.setItem('access_token', accessToken); // DON'TToken Storage
- Best: Server-side sessions with HTTP-only cookies
- Good: sessionStorage (cleared on tab close)
- Avoid: localStorage (persists across sessions)
- Never: Exposed JavaScript variables
Common Security Vulnerabilities
1. Authorization Code Interception
Attack: Attacker intercepts authorization code before it reaches your app.
Prevention:
- ✅ Use PKCE (mandatory for public clients)
- ✅ Short-lived codes (10 minutes)
- ✅ One-time use codes
// PKCE prevents this attack
const codeChallenge = generateCodeChallenge(codeVerifier);
// Even if attacker steals the code, they can't exchange it
// without the code_verifier2. Cross-Site Request Forgery (CSRF)
Attack: Attacker tricks user into authorizing their malicious app.
Prevention:
- ✅ Always use
stateparameter - ✅ Verify state matches stored value
- ✅ Use cryptographically random state
// Generate cryptographically secure state
const state = crypto.randomBytes(32).toString('hex');
// Verify in callback
if (callbackState !== storedState) {
throw new Error('CSRF attack detected');
}3. Token Leakage
Attack: Access tokens exposed through browser history, logs, or referrer headers.
Prevention:
- ✅ Use POST requests for sensitive data
- ✅ Never include tokens in URLs
- ✅ Clear sensitive data from browser history
// ✅ Good: Token in Authorization header
fetch('/api/user', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
// ❌ Bad: Token in URL
fetch(`/api/user?token=${accessToken}`); // Visible in logs!4. Open Redirect Vulnerability
Attack: Attacker manipulates redirect_uri to send user to malicious site.
Prevention:
- ✅ Whitelist exact redirect URIs
- ✅ No wildcard patterns
- ✅ Server validates redirect_uri
Flowsta automatically enforces this - you must pre-configure all redirect URIs in your app settings.
5. Insufficient Token Validation
Attack: Accepting expired, revoked, or forged tokens.
Prevention:
- ✅ Verify token signature (JWT)
- ✅ Check expiration (
expclaim) - ✅ Validate issuer (
issclaim) - ✅ Check token hasn't been revoked
import jwt from 'jsonwebtoken';
function validateAccessToken(token) {
try {
// Verify JWT signature and expiration
const decoded = jwt.verify(token, JWT_PUBLIC_KEY, {
issuer: 'https://auth-api.flowsta.com',
algorithms: ['HS256']
});
// Check custom claims
if (!decoded.userId || !decoded.clientId) {
throw new Error('Invalid token claims');
}
return decoded;
} catch (error) {
throw new Error('Token validation failed');
}
}Token Management
Access Token Lifecycle
graph LR
A[User Logs In] --> B[Issue Access Token]
B --> C{Token Valid?}
C -->|Yes| D[API Access]
C -->|No| E{Has Refresh Token?}
E -->|Yes| F[Refresh Access Token]
E -->|No| G[Re-authenticate]
F --> B
D --> CRefresh Token Security
Best Practices:
- Store securely (server-side or HTTP-only cookie)
- Rotate on use (issue new refresh token with each refresh)
- Revoke on logout (call
/oauth/revoke) - Limit lifetime (30 days max)
- One-time use (invalidate after exchange)
// Refresh access token
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://auth-api.flowsta.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.FLOWSTA_CLIENT_ID,
}),
});
const { access_token, refresh_token: newRefreshToken } = await response.json();
// Store new refresh token (rotation)
// Old refresh token is now invalid
return { accessToken: access_token, refreshToken: newRefreshToken };
}Token Revocation
Always revoke tokens on logout:
async function logout(refreshToken) {
// Revoke refresh token
await fetch('https://auth-api.flowsta.com/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': process.env.FLOWSTA_CLIENT_ID,
},
body: JSON.stringify({
token: refreshToken,
token_type_hint: 'refresh_token',
}),
});
// Clear local tokens
req.session.destroy();
res.clearCookie('access_token');
res.clearCookie('refresh_token');
}SSO Session Management
Flowsta uses HTTP-only cookies to enable seamless Single Sign-On (SSO) across all applications.
How SSO Sessions Work
When a user logs in through Flowsta (either directly or via OAuth), a secure flowsta_session cookie is created:
- HTTP-only: Cannot be accessed by JavaScript (protects against XSS)
- Secure: Only transmitted over HTTPS in production
- SameSite: Set to
Laxto prevent CSRF attacks - Domain:
.flowsta.com(shared across all Flowsta apps) - Duration: 7 days
This cookie enables users to seamlessly authenticate across multiple apps without re-entering credentials:
User Flow:
1. User logs into App A → flowsta_session cookie created
2. User visits App B → OAuth flow detects session cookie
3. App B auto-authorizes → No login prompt needed ✨Implementing Proper Logout
Critical: When implementing logout in your app, you must clear both local tokens AND the SSO session cookie.
If you only clear local storage, the SSO cookie remains active, causing unexpected behavior:
- User clicks "logout"
- User tries to login again
- OAuth flow auto-logs them in with the same account
- User cannot switch accounts!
Correct Logout Implementation
For Flowsta-hosted apps (using our SDK):
import { flowstaAuth } from '~/lib/flowsta-auth';
async function handleLogout() {
// This automatically:
// 1. Calls /auth/logout API endpoint (clears SSO cookie)
// 2. Clears localStorage tokens
// 3. Disconnects Holochain
await flowstaAuth.logout();
// Redirect to home
window.location.href = '/';
}For third-party apps (manual implementation):
async function handleLogout() {
const accessToken = getAccessToken(); // Your stored token
try {
// 1. Revoke OAuth refresh token
await fetch('https://auth-api.flowsta.com/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': 'your_client_id',
},
body: JSON.stringify({
token: refreshToken,
token_type_hint: 'refresh_token',
}),
});
// 2. Clear SSO session (if using first-party Flowsta auth)
// Note: This only works if your app uses Flowsta's auth API directly
await fetch('https://auth-api.flowsta.com/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('Logout error:', error);
// Continue with local logout even if API calls fail
}
// 3. Clear all local tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
sessionStorage.clear();
// 4. Redirect
window.location.href = '/';
}SSO Cookie Scope
The SSO session cookie is only cleared when calling /auth/logout. If you only call /oauth/revoke, the SSO cookie remains active.
For third-party apps using OAuth, this is expected behavior - the user stays logged into Flowsta but your app's access is revoked.
Session Expiration
The SSO session cookie expires after 7 days of inactivity. After expiration:
- User must re-authenticate with their Flowsta credentials
- OAuth flows will prompt for login
- Local tokens should be cleared when detecting an expired session
// Detect expired session
fetch('https://auth-api.flowsta.com/oauth/userinfo', {
headers: { 'Authorization': `Bearer ${accessToken}` }
})
.then(res => {
if (res.status === 401) {
// Session expired - clear local tokens
clearLocalTokens();
redirectToLogin();
}
});Multi-Account Switching
Users can explicitly logout and login with a different Flowsta account:
- User clicks "Logout" in your app
- Your app calls
/auth/logout(clears SSO cookie) - User clicks "Login with Flowsta"
- OAuth flow shows login screen (not auto-login)
- User enters different credentials
- New SSO session established
Testing Multi-Account
To test multi-account switching:
- Login with Account A
- Logout (verify
/auth/logoutis called) - Click "Login with Flowsta"
- You should see the login form, not auto-login
- Enter Account B credentials
Email Permission System
Flowsta's zero-knowledge architecture requires special handling for email access.
How It Works
- User's email is encrypted on Holochain with their password
- Even Flowsta cannot decrypt it
- Apps requesting
emailscope need explicit user permission - User must approve email access via their Flowsta account
Implementation
// Request email scope
const authUrl = buildAuthUrl({
scope: 'profile email',
// ...
});
// Handle case where email is not returned
const userInfo = await fetchUserInfo(accessToken);
if (!userInfo.email) {
// User hasn't granted email permission
showEmailPermissionPrompt();
}Email Permission Flow
sequenceDiagram
participant App
participant Flowsta
participant User
App->>Flowsta: Request 'profile email' scopes
Flowsta->>User: Show consent screen (lists requested scopes)
User->>Flowsta: Click "Authorize" (approves all scopes)
Flowsta->>Flowsta: Grant email permission for this app
Flowsta->>App: Authorization code
App->>Flowsta: Exchange code for access token
Flowsta->>App: Access token with 'profile email' scopes
App->>Flowsta: /oauth/userinfo
Flowsta->>App: Returns profile + email ✅Consent Screen Grants Email
When a user clicks "Authorize" on the consent screen, email permission is granted immediately. No separate step is needed.
Optional Email
Design your app to work without email if possible. Not all users will grant email permission due to Flowsta's privacy-first approach.
Production Checklist
Before Deploying
- [ ] PKCE implemented for all clients
- [ ] State parameter used for CSRF protection
- [ ] HTTPS used for all redirect URIs
- [ ] Redirect URIs whitelisted in app settings
- [ ] Tokens stored in HTTP-only cookies or server-side sessions
- [ ] Token validation implemented (expiration, signature, claims)
- [ ] Refresh token rotation implemented
- [ ] Logout revokes refresh tokens and clears SSO session
- [ ] Multi-account switching tested (logout → login with different account)
- [ ] Error handling doesn't leak sensitive information
- [ ] Rate limiting respected (check
X-RateLimit-*headers) - [ ] Audit logging enabled in developer dashboard
Security Headers
Add security headers to your responses:
app.use((req, res, next) => {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Enable XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// HTTPS only
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Content Security Policy
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'");
next();
});Monitoring & Alerts
Enable security monitoring:
Check audit logs in developer dashboard regularly
Monitor for suspicious patterns:
- Multiple failed auth attempts
- Unusual IP addresses
- High token refresh rates
- Failed token validations
Set up alerts for:
- Rate limit exceeded
- Invalid client credentials
- Token revocation spikes
Common Mistakes to Avoid
1. Skipping State Verification
// ❌ DON'T DO THIS
app.get('/callback', (req, res) => {
const { code } = req.query;
// No state verification! Vulnerable to CSRF
exchangeCodeForToken(code);
});Fix: Always verify the state parameter.
2. Using localStorage for Tokens
// ❌ DON'T DO THIS
localStorage.setItem('access_token', token);
// Persists across sessions, vulnerable to XSSFix: Use sessionStorage or HTTP-only cookies.
3. Not Using PKCE
// ❌ DON'T DO THIS
const authUrl = buildAuthUrl({
// No PKCE! Vulnerable to code interception
});Fix: Always use PKCE - it's required for all Flowsta OAuth flows.
4. Ignoring Token Expiration
// ❌ DON'T DO THIS
const token = getStoredToken();
await api.call(token); // Might be expired!Fix: Check expiration and refresh if needed.
Incident Response
If You Suspect a Breach
Immediately revoke all refresh tokens:
javascriptawait revokeAllUserTokens(userId);Notify users if personal data was exposed
Review audit logs for suspicious activity
Contact support: security@flowsta.com
No Client Secrets to Regenerate
With PKCE, there are no client secrets to manage or rotate. Each OAuth flow generates a unique code_verifier per session, providing security without long-lived secrets.
Compliance & Standards
Standards Supported
- ✅ OAuth 2.0 (RFC 6749)
- ✅ PKCE (RFC 7636)
- ✅ JWT (RFC 7519)
- ✅ W3C DIDs (Decentralized Identifiers)
Privacy Compliance
- GDPR compliant - User data stored with consent
- Zero-knowledge architecture - Flowsta cannot access encrypted data
- Right to be forgotten - Users can delete their accounts
- Data portability - DIDs are portable and user-owned
Additional Resources
Official Specifications
Security Guides
Need Help?
- 💬 Discord: Join our community
- 🆘 Support: Find out about Flowsta support options
- 🐙 GitHub: github.com/WeAreFlowsta