Security Guide
Security measures implemented in the blah.dev OAuth provider and best practices for integrating clients.
Cryptographic Standards
Password Hashing
Passwords are hashed using Argon2id, the winner of the Password Hashing Competition and recommended by OWASP.
const ARGON2_OPTIONS = {
memoryCost: 65536, // 64 MB memory
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel threads
outputLen: 32, // 256-bit output
};GPU Resistant
Memory-hard algorithm
Side-Channel Safe
Resistant to timing attacks
Configurable
Memory/time tradeoffs
Well Audited
Modern, peer-reviewed algorithm
Token Storage
Tokens are never stored in plaintext. Before storage, all tokens are hashed using SHA-256:
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}This means a database breach doesn't expose usable tokens — tokens cannot be reverse-engineered from their hashes.
Token Generation
| Token Type | Length | Encoding |
|---|---|---|
| Client ID | 16 bytes | base64url |
| Client Secret | 32 bytes | base64url |
| Authorization Code | 32 bytes | base64url |
| Access Token | 32 bytes | base64url |
| Refresh Token | 32 bytes | base64url |
| CSRF Token | 32 bytes | base64url |
Token Security
Token Lifetimes
| Token | Lifetime | Rationale |
|---|---|---|
| Authorization Code | 10 minutes | Short-lived, single-use |
| Access Token | 1 hour | Limits exposure window |
| Refresh Token | 30 days | Convenience vs. security balance |
| Session | 7 days | User experience |
Single-Use Authorization Codes
Authorization codes are marked as used immediately after successful token exchange. This prevents replay attacks where an attacker intercepts the code during redirect.
Refresh Token Rotation
Each time a refresh token is used, a new one is issued and the old one is revoked. This limits exposure if a refresh token is leaked and enables detection of token theft.
Token Family Revocation
When a refresh token is revoked, all associated access tokens are also revoked. This ensures logout is complete and no orphaned access tokens remain.
Attack Prevention
CSRF Protection
The consent form is protected with server-side CSRF tokens stored in encrypted session cookies. Clients should also always use the state parameter.
// Server validates CSRF token on consent submission
if (session.csrfToken !== submittedToken) {
return error('invalid_request', 'Invalid CSRF token');
}
// Client validates state on callback
if (returnedState !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch - possible CSRF attack');
}PKCE (Proof Key for Code Exchange)
PKCE prevents authorization code interception attacks. The client generates a random verifier, creates a SHA-256 challenge, and proves knowledge of the verifier during token exchange.
// Client: generate verifier and challenge
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
// Send challenge in authorization request
// Send verifier in token request
// Server validates: SHA256(verifier) === challengeRedirect URI Validation
Redirect URIs must exactly match a registered URI. No wildcards or partial matching. URIs must use HTTPS (except localhost for development). Maximum 10 URIs per client.
Token Enumeration Prevention
The introspection endpoint returns { "active": false } for all invalid tokens with no distinction between “invalid” and “expired.” The revocation endpoint always returns 200 OK regardless of whether the token existed.
Client Security
Confidential Clients
Server-side apps that can securely store a client secret. Must authenticate with secret on token requests. Secret verified with Argon2id.
Public Clients
SPAs and mobile apps that cannot securely store secrets. Must use PKCE for authorization. No client secret verification.
Secret Storage Recommendations
- Store secrets in environment variables
- Never commit secrets to version control
- Use a secrets manager in production (AWS Secrets Manager, HashiCorp Vault)
- Rotate secrets periodically
Session Security
Sessions use iron-session with AES-256-GCM encryption and HMAC integrity verification. Cookies are HttpOnly (not accessible to JavaScript), Secure in production, and SameSite=Lax.
On login, a new session is always created (session fixation prevention). On logout, the session is deleted from the database and the cookie is cleared.
Security Checklist for Integrators
Required
- Use HTTPS for all OAuth endpoints
- Store client secrets securely (not in code)
- Validate state parameter on callback
- Use PKCE for public clients
- Validate token responses before use
- Handle token expiration gracefully
Recommended
- Use PKCE for all clients (even confidential)
- Implement token refresh before expiration
- Log security-relevant events
- Monitor for unusual activity
- Use secure cookie settings for tokens
Avoid
- Storing tokens in localStorage (XSS vulnerable)
- Including tokens in URLs
- Logging tokens or secrets
- Disabling TLS certificate verification
- Using "plain" PKCE method (prefer S256)
Incident Response
Suspected Token Compromise
- Revoke immediately via the revocation endpoint
- Rotate client secret if client credentials may be compromised
- Review access logs for unauthorized activity
- Notify affected users if data exposure occurred
Suspected Client Credential Compromise
- Delete the OAuth client immediately
- Create new client with new credentials
- Update application with new credentials
- Revoke all tokens issued to old client
- Audit access during compromise window