Authentication Security¶
Token storage, PKCE, token lifetimes, and server-side validation.
Token Storage¶
| Storage | Use For | ⚠️ Risk |
|---|---|---|
| Memory | Access tokens | Lost on page refresh |
| HttpOnly Cookie | Refresh tokens | Requires HTTPS |
| sessionStorage | PKCE verifier | Cleared on tab close |
| ❌ localStorage | Nothing sensitive! | XSS vulnerable |
// ✅ Good: Token in memory, refresh via httpOnly cookie
// After OAuth callback, your server exchanges the code:
const tokens = await exchangeCodeForTokens({
authVitalHost: process.env.AV_HOST!,
code: authorizationCode,
codeVerifier,
redirectUri,
clientId: process.env.AV_CLIENT_ID!,
clientSecret: process.env.AV_CLIENT_SECRET!,
});
// Store accessToken in React state or module-level variable (NOT localStorage!)
// ❌ Bad: Never store tokens in localStorage
localStorage.setItem('token', accessToken); // XSS risk!
PKCE for SPAs¶
Always use PKCE for browser-based applications:
import { generatePKCE, buildAuthorizeUrl } from '@authvital/sdk/server';
// ✅ Required for SPAs
const { codeVerifier, codeChallenge } = await generatePKCE();
const authorizeUrl = buildAuthorizeUrl({
// ...
codeChallenge,
codeChallengeMethod: 'S256',
});
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks:
- Generate verifier: Random 43-128 character string
- Create challenge: SHA256 hash of verifier, base64url encoded
- Send challenge: Include in authorize request
- Verify on exchange: AuthVital verifies your verifier matches the challenge
Token Lifetimes¶
| Token | Recommended | Maximum |
|---|---|---|
| Access Token | 15-60 min | 1 hour |
| Refresh Token | 7 days | 30 days |
| Auth Code | 1-5 min | 10 min |
Shorter lifetimes = smaller window for stolen tokens.
Validate on Server¶
Never trust client-side checks alone:
// ✅ Server validates JWT and permissions
app.get('/api/admin', async (req, res) => {
const { authenticated, user } = await authvital.getCurrentUser(req);
if (!authenticated) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Use SDK's wildcard-aware permission check
if (!await authvital.hasAppPermission(req, 'admin:*')) {
return res.status(403).json({ error: 'Forbidden' });
}
// ...
});
// ❌ Don't rely only on UI hiding
// UI can be bypassed, API must enforce
Input Validation¶
Sanitize User Input¶
import { escape } from 'lodash';
// ✅ Sanitize before storing/displaying
const safeName = escape(userInput.name);
// ✅ Use parameterized queries (Prisma does this automatically)
const user = await prisma.user.findUnique({
where: { email: userEmail }, // Safe from SQL injection
});
// ❌ Never concatenate user input into queries
// const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
Validate Email Format¶
// ✅ Validate email format
import { z } from 'zod';
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
// In NestJS, use class-validator
class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
}