Security best practices
1. Always use HTTPS
All communication with Klubero SSO must use HTTPS. HTTP requests will be rejected.
✓ https://your-sso-domain.com/connect/authorize
✗ http://your-sso-domain.com/connect/authorize
2. Implement PKCE for public clients
Single Page Applications and mobile apps must use PKCE:
// Generate a code verifier and challenge
const verifier = generateCodeVerifier(); // Random string, 43-128 characters
const challenge = await sha256(verifier); // SHA256 hash
// Include in the authorization request
const authUrl = `https://your-sso-domain.com/connect/authorize?
code_challenge=${challenge}&
code_challenge_method=S256`;
// Include the verifier in the token request
const tokenResponse = await fetch('/connect/token', {
body: `code_verifier=${verifier}&...`
});
3. Validate the state parameter
Always generate and validate the state parameter to prevent CSRF attacks:
// Before redirecting
const state = generateRandomString(32);
sessionStorage.setItem('oauth_state', state);
// After the callback
const returnedState = new URLSearchParams(location.search).get('state');
const savedState = sessionStorage.getItem('oauth_state');
if (returnedState !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}
4. Store tokens securely
| Client type | Storage recommendation |
|---|---|
| Server-side application | Server-side session or an encrypted cookie |
| SPA | Memory (not localStorage), or an httpOnly cookie via a BFF |
| Mobile app | Secure keychain/keystore |
| Desktop application | OS credential manager |
Never store tokens in
- localStorage (vulnerable to XSS)
- Plain cookies (vulnerable to CSRF)
- URL parameters
- Browser history
5. Never expose the client secret
The client secret must never be exposed in:
- Frontend/client-side code
- Version control (use environment variables)
- Logs
- Error messages
- URLs
6. Validate tokens
Always validate tokens before trusting them:
// 1. Verify the signature using JWKS
// 2. Check that the issuer (iss) matches your SSO server
// 3. Check that the audience (aud) matches your client_id
// 4. Check that the expiration (exp) is in the future
// 5. Check that the nonce matches (if you use one)
7. Handle token expiration
function isTokenExpired(token, bufferSeconds = 60) {
const payload = JSON.parse(atob(token.split('.')[1]));
const expiresAt = payload.exp * 1000;
return Date.now() >= expiresAt - (bufferSeconds * 1000);
}
// Refresh proactively before expiration
if (isTokenExpired(accessToken, 300)) { // 5-min buffer
accessToken = await refreshToken();
}
8. Use minimal scopes
Request only the scopes your application actually needs:
# Good - minimal scopes
scope=openid profile email
# Avoid - requesting everything
scope=openid profile email phone address offline_access
9. Implement proper sign-out
When signing out:
- Revoke the refresh token (on the server)
- Clear all stored tokens
- Redirect to the SSO logout endpoint
- Clear the application session
10. Monitor security events
Watch for these events, which may indicate security issues:
- Multiple failed sign-in attempts
- Token refresh from a new IP/device
- Unusual scope requests
- Sessions from an unexpected location