> **Building with AI coding agents?** If you're using an AI coding agent, install the official Scalekit plugin. It gives your agent full awareness of the Scalekit API — reducing hallucinations and enabling faster, more accurate code generation.
>
> - **Claude Code**: `/plugin marketplace add scalekit-inc/claude-code-authstack` then `/plugin install <auth-type>@scalekit-auth-stack`
> - **GitHub Copilot CLI**: `copilot plugin marketplace add scalekit-inc/github-copilot-authstack` then `copilot plugin install <auth-type>@scalekit-auth-stack`
> - **Codex**: run the bash installer, restart, then open Plugin Directory and enable `<auth-type>`
> - **Skills CLI** (Windsurf, Cline, 40+ agents): `npx skills add scalekit-inc/skills --list` then `--skill <skill-name>`
>
> `<auth-type>` / `<skill-name>`: `agentkit`, `full-stack-auth`, `mcp-auth`, `modular-sso`, `modular-scim` — [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Authentication best practices

Security best practices for authentication implementation, including threat modeling, advanced patterns, and security checklists.
This guide covers security best practices for implementing authentication with Scalekit. Use it for threat modeling, advanced security patterns, and production-ready configurations.

## Security threat model

### Common authentication threats

Identify potential security threats to implement appropriate countermeasures:

| Threat | Description | Mitigation |
|--------|-------------|------------|
| **CSRF attacks** | Malicious requests executed on behalf of authenticated users | Use `state` parameter, validate origins |
| **Token theft** | Access tokens intercepted or stolen | Secure storage, short lifetimes, refresh rotation |
| **Session fixation** | Attacker fixes session ID before authentication | Regenerate sessions, secure cookies |
| **Phishing** | Users tricked into entering credentials on fake sites | Domain validation, HTTPS enforcement |
| **Replay attacks** | Intercepted requests replayed by attackers | Nonces, timestamps, request signing |

### Multi-tenant security considerations

B2B applications face additional security challenges:

- **Tenant isolation** - Prevent data leakage between organizations
- **Admin privilege escalation** - Secure organization admin roles
- **SSO configuration tampering** - Protect identity provider settings
- **Cross-tenant user enumeration** - Prevent user discovery across organizations

## Advanced security patterns

### Dynamic security policy enforcement

Apply organization-specific security policies:

### Node.js

```javascript title="Dynamic security policies" {5-8,15-18} "getSecurityPolicy"
// Apply organization-specific security requirements
async function createAuthorizationUrl(orgId, userEmail) {
  const redirectUri = 'https://yourapp.com/auth/callback';

  // Fetch organization security policy
  const securityPolicy = await getSecurityPolicy(orgId);

  // Apply conditional authentication requirements
  const options = {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    organizationId: orgId,
    loginHint: userEmail,
    state: generateSecureState(),

    // Force re-authentication for high-security orgs
    prompt: securityPolicy.requireReauth ? 'login' : undefined,
    maxAge: securityPolicy.maxSessionAge || 3600,
    acrValues: securityPolicy.requiredAuthLevel || 'aal1'
  };

  return scalekit.getAuthorizationUrl(redirectUri, options);
}
```

### Python

```python title="Dynamic security policies" {5-8,15-18} "get_security_policy"
# Apply organization-specific security requirements
async def create_authorization_url(org_id, user_email):
    redirect_uri = 'https://yourapp.com/auth/callback'

    # Fetch organization security policy
    security_policy = await get_security_policy(org_id)

    # Apply conditional authentication requirements
    options = AuthorizationUrlOptions(
        scopes=['openid', 'profile', 'email', 'offline_access'],
        organization_id=org_id,
        login_hint=user_email,
        state=generate_secure_state(),

        # Force re-authentication for high-security orgs
        prompt='login' if security_policy.require_reauth else None,
        max_age=security_policy.max_session_age or 3600,
        acr_values=security_policy.required_auth_level or 'aal1'
    )

    return scalekit.get_authorization_url(redirect_uri, options)
```

### Go

```go title="Dynamic security policies" {5-8,15-18} "GetSecurityPolicy"
// Apply organization-specific security requirements
func createAuthorizationUrl(orgId, userEmail string) (string, error) {
	redirectUri := "https://yourapp.com/auth/callback"

	// Fetch organization security policy
	securityPolicy, err := getSecurityPolicy(orgId)
	if err != nil {
		return "", err
	}

	// Apply conditional authentication requirements
	options := scalekit.AuthorizationUrlOptions{
		Scopes: []string{"openid", "profile", "email", "offline_access"},
		OrganizationId: orgId,
		LoginHint: userEmail,
		State: generateSecureState(),

		// Force re-authentication for high-security orgs
		Prompt: conditionalPrompt(securityPolicy.RequireReauth),
		MaxAge: securityPolicy.MaxSessionAge,
		AcrValues: securityPolicy.RequiredAuthLevel,
	}

	authUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)
	return authUrl.String(), err
}
```

### Java

```java title="Dynamic security policies" {5-8,15-18} "getSecurityPolicy"
// Apply organization-specific security requirements
public String createAuthorizationUrl(String orgId, String userEmail) {
    String redirectUri = "https://yourapp.com/auth/callback";

    // Fetch organization security policy
    SecurityPolicy securityPolicy = getSecurityPolicy(orgId);

    // Apply conditional authentication requirements
    AuthorizationUrlOptions options = new AuthorizationUrlOptions();
    options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));
    options.setOrganizationId(orgId);
    options.setLoginHint(userEmail);
    options.setState(generateSecureState());

    // Force re-authentication for high-security orgs
    if (securityPolicy.isRequireReauth()) {
        options.setPrompt("login");
    }
    options.setMaxAge(securityPolicy.getMaxSessionAge());
    options.setAcrValues(securityPolicy.getRequiredAuthLevel());

    URL authUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options);
    return authUrl.toString();
}
```

### Request signing and validation

Verify request integrity with signatures:

### Node.js

```javascript title="Request signing" {8-12,20-24} "signRequest"
const crypto = require('crypto');

// Sign sensitive requests with HMAC
function signRequest(payload, secret) {
  const timestamp = Date.now().toString();
  const nonce = crypto.randomBytes(16).toString('hex');

  // Create signature payload
  const signaturePayload = `${timestamp}.${nonce}.${JSON.stringify(payload)}`;
  const signature = crypto
    .createHmac('sha256', secret)
    .update(signaturePayload)
    .digest('hex');

  return {
    payload,
    timestamp,
    nonce,
    signature: `sha256=${signature}`
  };
}

// Verify request signatures
function verifyRequest(receivedPayload, receivedSignature, secret, maxAge = 300) {
  const [timestamp, nonce, payload] = receivedPayload.split('.');

  // Check timestamp to prevent replay attacks
  if (Date.now() - parseInt(timestamp) > maxAge * 1000) {
    throw new Error('Request timestamp too old');
  }

  // Verify signature
  const expectedPayload = `${timestamp}.${nonce}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(expectedPayload)
    .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(receivedSignature, 'hex'),
    Buffer.from(`sha256=${expectedSignature}`, 'hex')
  )) {
    throw new Error('Invalid signature');
  }

  return JSON.parse(payload);
}
```

### Python

```python title="Request signing" {9-13,23-27} "sign_request"

# Sign sensitive requests with HMAC
def sign_request(payload, secret):
    timestamp = str(int(time.time() * 1000))
    nonce = secrets.token_hex(16)

    # Create signature payload
    signature_payload = f"{timestamp}.{nonce}.{json.dumps(payload)}"
    signature = hmac.new(
        secret.encode(),
        signature_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return {
        'payload': payload,
        'timestamp': timestamp,
        'nonce': nonce,
        'signature': f"sha256={signature}"
    }

# Verify request signatures
def verify_request(received_payload, received_signature, secret, max_age=300):
    timestamp, nonce, payload = received_payload.split('.')

    # Check timestamp to prevent replay attacks
    if time.time() * 1000 - int(timestamp) > max_age * 1000:
        raise ValueError('Request timestamp too old')

    # Verify signature
    expected_payload = f"{timestamp}.{nonce}.{payload}"
    expected_signature = hmac.new(
        secret.encode(),
        expected_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(
        received_signature,
        f"sha256={expected_signature}"
    ):
        raise ValueError('Invalid signature')

    return json.loads(payload)
```

### Go

```go title="Request signing" {10-16,25-29} "SignRequest"

	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"
)

// Sign sensitive requests with HMAC
func signRequest(payload interface{}, secret string) (map[string]interface{}, error) {
	timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())

	nonceBytes := make([]byte, 16)
	rand.Read(nonceBytes)
	nonce := hex.EncodeToString(nonceBytes)

	// Create signature payload
	payloadJSON, _ := json.Marshal(payload)
	signaturePayload := fmt.Sprintf("%s.%s.%s", timestamp, nonce, payloadJSON)

	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(signaturePayload))
	signature := hex.EncodeToString(h.Sum(nil))

	return map[string]interface{}{
		"payload":   payload,
		"timestamp": timestamp,
		"nonce":     nonce,
		"signature": fmt.Sprintf("sha256=%s", signature),
	}, nil
}

// Verify request signatures
func verifyRequest(receivedPayload, receivedSignature, secret string, maxAge int64) (interface{}, error) {
	// Parse payload components
	parts := strings.Split(receivedPayload, ".")
	if len(parts) != 3 {
		return nil, fmt.Errorf("invalid payload format")
	}

	timestamp, err := strconv.ParseInt(parts[0], 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid timestamp")
	}

	// Check timestamp to prevent replay attacks
	if time.Now().UnixMilli()-timestamp > maxAge*1000 {
		return nil, fmt.Errorf("request timestamp too old")
	}

	// Verify signature
	expectedPayload := receivedPayload
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(expectedPayload))
	expectedSignature := fmt.Sprintf("sha256=%s", hex.EncodeToString(h.Sum(nil)))

	if !hmac.Equal([]byte(receivedSignature), []byte(expectedSignature)) {
		return nil, fmt.Errorf("invalid signature")
	}

	var payload interface{}
	if err := json.Unmarshal([]byte(parts[2]), &payload); err != nil {
		return nil, fmt.Errorf("invalid payload JSON")
	}

	return payload, nil
}
```

### Java

```java title="Request signing" {11-17,26-30} "signRequest"

// Sign sensitive requests with HMAC
public Map signRequest(Object payload, String secret) throws Exception {
    String timestamp = String.valueOf(System.currentTimeMillis());

    SecureRandom random = new SecureRandom();
    byte[] nonceBytes = new byte[16];
    random.nextBytes(nonceBytes);
    String nonce = bytesToHex(nonceBytes);

    // Create signature payload
    String payloadJson = objectMapper.writeValueAsString(payload);
    String signaturePayload = timestamp + "." + nonce + "." + payloadJson;

    Mac mac = Mac.getInstance("HmacSHA256");
    SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    mac.init(secretKey);
    byte[] signatureBytes = mac.doFinal(signaturePayload.getBytes(StandardCharsets.UTF_8));
    String signature = "sha256=" + bytesToHex(signatureBytes);

    Map result = new HashMap<>();
    result.put("payload", payload);
    result.put("timestamp", timestamp);
    result.put("nonce", nonce);
    result.put("signature", signature);

    return result;
}

// Verify request signatures
public Object verifyRequest(String receivedPayload, String receivedSignature,
                           String secret, long maxAge) throws Exception {
    String[] parts = receivedPayload.split("\\.");
    if (parts.length != 3) {
        throw new SecurityException("Invalid payload format");
    }

    long timestamp = Long.parseLong(parts[0]);

    // Check timestamp to prevent replay attacks
    if (System.currentTimeMillis() - timestamp > maxAge * 1000) {
        throw new SecurityException("Request timestamp too old");
    }

    // Verify signature
    Mac mac = Mac.getInstance("HmacSHA256");
    SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    mac.init(secretKey);
    byte[] expectedSignatureBytes = mac.doFinal(receivedPayload.getBytes(StandardCharsets.UTF_8));
    String expectedSignature = "sha256=" + bytesToHex(expectedSignatureBytes);

    if (!MessageDigest.isEqual(
        receivedSignature.getBytes(StandardCharsets.UTF_8),
        expectedSignature.getBytes(StandardCharsets.UTF_8)
    )) {
        throw new SecurityException("Invalid signature");
    }

    return objectMapper.readValue(parts[2], Object.class);
}
```

## Secure token management

### Token storage strategies

Select storage methods based on your application architecture:

| Storage Method | Security Level | Use Case | Considerations |
|----------------|----------------|----------|----------------|
| **HTTP-only cookies** | High | Web applications | Prevents XSS, requires CSRF protection |
| **Secure memory** | High | Mobile/desktop apps | Cleared on app termination |
| **Encrypted storage** | Medium | Persistent sessions | Key management complexity |
| **LocalStorage** | Low | Not recommended | Vulnerable to XSS attacks |

### Token rotation implementation

Implement secure refresh token rotation:

### Node.js

```javascript title="Token rotation" {15-20,30-35} "rotateTokens"
// Secure token refresh with rotation
async function refreshAccessToken(refreshToken, userId) {
  try {
    // Exchange refresh token for new tokens
    const tokenResponse = await scalekit.exchangeCodeForTokens({
      refresh_token: refreshToken,
      grant_type: 'refresh_token'
    });

    // Store new tokens securely
    const newTokens = {
      accessToken: tokenResponse.access_token,
      refreshToken: tokenResponse.refresh_token, // New refresh token
      expiresAt: Date.now() + (tokenResponse.expires_in * 1000),
      refreshExpiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
    };

    // Update token storage atomically
    await updateUserTokens(userId, newTokens);

    // Invalidate old refresh token
    await invalidateRefreshToken(refreshToken);

    return newTokens;

  } catch (error) {
    // Handle refresh failure
    if (error.code === 'invalid_grant') {
      // Refresh token expired or revoked
      await logoutUser(userId);
      throw new Error('Session expired, please login again');
    }

    // Log security event
    await logSecurityEvent('token_refresh_failed', {
      userId,
      error: error.message,
      timestamp: new Date().toISOString()
    });

    throw error;
  }
}

// Automatic token refresh middleware
function autoRefreshMiddleware(req, res, next) {
  const { accessToken, refreshToken, expiresAt } = req.session.tokens || {};

  // Check if token expires within 5 minutes
  if (accessToken && Date.now() + (5 * 60 * 1000) >= expiresAt) {
    refreshAccessToken(refreshToken, req.session.userId)
      .then(newTokens => {
        req.session.tokens = newTokens;
        next();
      })
      .catch(error => {
        // Clear session on refresh failure
        req.session.destroy();
        res.status(401).json({ error: 'Authentication required' });
      });
  } else {
    next();
  }
}
```

### Python

```python title="Token rotation" {16-21,33-38} "refresh_access_token"

from datetime import datetime, timedelta

# Secure token refresh with rotation
async def refresh_access_token(refresh_token, user_id):
    try:
        # Exchange refresh token for new tokens
        token_response = await scalekit.exchange_code_for_tokens({
            'refresh_token': refresh_token,
            'grant_type': 'refresh_token'
        })

        # Store new tokens securely
        new_tokens = {
            'access_token': token_response['access_token'],
            'refresh_token': token_response['refresh_token'],  # New refresh token
            'expires_at': datetime.now() + timedelta(seconds=token_response['expires_in']),
            'refresh_expires_at': datetime.now() + timedelta(days=30)
        }

        # Update token storage atomically
        await update_user_tokens(user_id, new_tokens)

        # Invalidate old refresh token
        await invalidate_refresh_token(refresh_token)

        return new_tokens

    except Exception as error:
        # Handle refresh failure
        if hasattr(error, 'code') and error.code == 'invalid_grant':
            # Refresh token expired or revoked
            await logout_user(user_id)
            raise Exception('Session expired, please login again')

        # Log security event
        await log_security_event('token_refresh_failed', {
            'user_id': user_id,
            'error': str(error),
            'timestamp': datetime.now().isoformat()
        })

        raise error

# Automatic token refresh decorator
def auto_refresh_tokens(func):
    async def wrapper(*args, **kwargs):
        request = kwargs.get('request') or args[0]
        tokens = getattr(request.session, 'tokens', {})

        access_token = tokens.get('access_token')
        refresh_token = tokens.get('refresh_token')
        expires_at = tokens.get('expires_at')

        # Check if token expires within 5 minutes
        if access_token and expires_at and datetime.now() + timedelta(minutes=5) >= expires_at:
            try:
                new_tokens = await refresh_access_token(refresh_token, request.session.user_id)
                request.session.tokens = new_tokens
            except Exception:
                # Clear session on refresh failure
                request.session.clear()
                raise AuthenticationError('Authentication required')

        return await func(*args, **kwargs)
    return wrapper
```

### Go

```go title="Token rotation" {17-23,35-40} "RefreshAccessToken"

	"context"
	"fmt"
	"time"
)

type TokenSet struct {
	AccessToken       string    `json:"access_token"`
	RefreshToken      string    `json:"refresh_token"`
	ExpiresAt         time.Time `json:"expires_at"`
	RefreshExpiresAt  time.Time `json:"refresh_expires_at"`
}

// Secure token refresh with rotation
func refreshAccessToken(ctx context.Context, refreshToken, userID string) (*TokenSet, error) {
	// Exchange refresh token for new tokens
	tokenResponse, err := scalekit.ExchangeCodeForTokens(ctx, &scalekit.TokenRequest{
		RefreshToken: refreshToken,
		GrantType:    "refresh_token",
	})
	if err != nil {
		return nil, fmt.Errorf("token exchange failed: %w", err)
	}

	// Store new tokens securely
	newTokens := &TokenSet{
		AccessToken:      tokenResponse.AccessToken,
		RefreshToken:     tokenResponse.RefreshToken, // New refresh token
		ExpiresAt:        time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second),
		RefreshExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
	}

	// Update token storage atomically
	if err := updateUserTokens(ctx, userID, newTokens); err != nil {
		return nil, fmt.Errorf("failed to update tokens: %w", err)
	}

	// Invalidate old refresh token
	if err := invalidateRefreshToken(ctx, refreshToken); err != nil {
		// Log but don't fail the operation
		logSecurityEvent(ctx, "refresh_token_invalidation_failed", map[string]interface{}{
			"user_id": userID,
			"error":   err.Error(),
		})
	}

	return newTokens, nil
}

// Automatic token refresh middleware
func autoRefreshMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		session := getSession(r)
		tokens := session.Tokens

		// Check if token expires within 5 minutes
		if tokens != nil && time.Until(tokens.ExpiresAt) <= 5*time.Minute {
			newTokens, err := refreshAccessToken(r.Context(), tokens.RefreshToken, session.UserID)
			if err != nil {
				// Clear session on refresh failure
				clearSession(w, r)
				http.Error(w, "Authentication required", http.StatusUnauthorized)
				return
			}

			session.Tokens = newTokens
			saveSession(w, r, session)
		}

		next.ServeHTTP(w, r)
	})
}
```

### Java

```java title="Token rotation" {18-24,38-43} "refreshAccessToken"

public class TokenSet {
    private String accessToken;
    private String refreshToken;
    private Instant expiresAt;
    private Instant refreshExpiresAt;

    // constructors, getters, setters...
}

// Secure token refresh with rotation
public CompletableFuture refreshAccessToken(String refreshToken, String userId) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            // Exchange refresh token for new tokens
            TokenResponse tokenResponse = scalekit.authentication()
                .exchangeCodeForTokens(TokenRequest.builder()
                    .refreshToken(refreshToken)
                    .grantType("refresh_token")
                    .build());

            // Store new tokens securely
            TokenSet newTokens = new TokenSet();
            newTokens.setAccessToken(tokenResponse.getAccessToken());
            newTokens.setRefreshToken(tokenResponse.getRefreshToken()); // New refresh token
            newTokens.setExpiresAt(Instant.now().plusSeconds(tokenResponse.getExpiresIn()));
            newTokens.setRefreshExpiresAt(Instant.now().plus(30, ChronoUnit.DAYS));

            // Update token storage atomically
            updateUserTokens(userId, newTokens);

            // Invalidate old refresh token
            invalidateRefreshToken(refreshToken);

            return newTokens;

        } catch (Exception e) {
            // Handle refresh failure
            if (e instanceof InvalidGrantException) {
                // Refresh token expired or revoked
                logoutUser(userId);
                throw new AuthenticationException("Session expired, please login again");
            }

            // Log security event
            logSecurityEvent("token_refresh_failed", Map.of(
                "user_id", userId,
                "error", e.getMessage(),
                "timestamp", Instant.now().toString()
            ));

            throw new RuntimeException(e);
        }
    });
}

// Automatic token refresh interceptor
@Component
public class AutoRefreshInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session == null) return true;

        TokenSet tokens = (TokenSet) session.getAttribute("tokens");
        if (tokens == null) return true;

        // Check if token expires within 5 minutes
        if (tokens.getExpiresAt().minus(5, ChronoUnit.MINUTES).isBefore(Instant.now())) {
            try {
                String userId = (String) session.getAttribute("userId");
                TokenSet newTokens = refreshAccessToken(tokens.getRefreshToken(), userId).get();
                session.setAttribute("tokens", newTokens);
            } catch (Exception e) {
                // Clear session on refresh failure
                session.invalidate();
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("{\"error\":\"Authentication required\"}");
                return false;
            }
        }

        return true;
    }
}
```

## Security monitoring and incident response

### Security event logging

Log security events for monitoring and analysis:

```javascript title="security-events.js"
// Define security event types
const SECURITY_EVENTS = {
  LOGIN_SUCCESS: 'login_success',
  LOGIN_FAILURE: 'login_failure',
  TOKEN_REFRESH: 'token_refresh',
  SUSPICIOUS_ACTIVITY: 'suspicious_activity',
  PRIVILEGE_ESCALATION: 'privilege_escalation',
  DATA_ACCESS: 'sensitive_data_access'
};

// Security event logger
async function logSecurityEvent(eventType, details) {
  const event = {
    type: eventType,
    timestamp: new Date().toISOString(),
    severity: getSeverityLevel(eventType),
    details: {
      ...details,
      userAgent: details.userAgent,
      ipAddress: details.ipAddress,
      sessionId: details.sessionId
    }
  };

  // Store in security log
  await securityLogger.log(event);

  // Trigger alerts for high-severity events
  if (event.severity === 'HIGH' || event.severity === 'CRITICAL') {
    await triggerSecurityAlert(event);
  }
}

// Anomaly detection
async function detectAnomalies(userId, loginEvent) {
  const recentLogins = await getRecentLogins(userId, '24h');

  // Check for unusual patterns
  const anomalies = [];

  // Geographic anomaly
  if (isUnusualLocation(loginEvent.location, recentLogins)) {
    anomalies.push('unusual_location');
  }

  // Time-based anomaly
  if (isUnusualTime(loginEvent.timestamp, recentLogins)) {
    anomalies.push('unusual_time');
  }

  // Device anomaly
  if (isUnusualDevice(loginEvent.device, recentLogins)) {
    anomalies.push('unusual_device');
  }

  if (anomalies.length > 0) {
    await logSecurityEvent(SECURITY_EVENTS.SUSPICIOUS_ACTIVITY, {
      userId,
      anomalies,
      loginEvent
    });
  }

  return anomalies;
}
```

### Rate limiting and abuse prevention

Apply rate limiting to prevent abuse:

### Node.js

```javascript title="Advanced rate limiting" {10-15,25-30} "RateLimiter"
// Multi-tier rate limiting
class SecurityRateLimiter {
  constructor() {
    this.limits = {
      // Per-IP limits
      login_attempts: { window: 900, max: 10 }, // 10 attempts per 15 min
      token_requests: { window: 3600, max: 100 }, // 100 requests per hour

      // Per-user limits
      user_login_attempts: { window: 3600, max: 5 }, // 5 attempts per hour
      user_token_refresh: { window: 3600, max: 50 }, // 50 refreshes per hour

      // Global limits
      total_requests: { window: 60, max: 10000 } // 10k requests per minute
    };
  }

  async checkLimit(type, identifier, customLimit = null) {
    const limit = customLimit || this.limits[type];
    if (!limit) return { allowed: true };

    const key = `${type}:${identifier}`;
    const current = await redis.get(key) || 0;

    if (current >= limit.max) {
      await this.logRateLimitExceeded(type, identifier, current);
      return {
        allowed: false,
        retryAfter: await redis.ttl(key),
        current: current,
        max: limit.max
      };
    }

    // Increment counter with expiration
    await redis.multi()
      .incr(key)
      .expire(key, limit.window)
      .exec();

    return { allowed: true, current: current + 1, max: limit.max };
  }

  // Dynamic rate limiting based on risk
  async getDynamicLimit(type, riskScore) {
    const baseLimit = this.limits[type];
    if (riskScore > 0.8) {
      return { ...baseLimit, max: Math.floor(baseLimit.max * 0.2) };
    } else if (riskScore > 0.6) {
      return { ...baseLimit, max: Math.floor(baseLimit.max * 0.5) };
    }
    return baseLimit;
  }
}

// Rate limiting middleware
async function rateLimitMiddleware(req, res, next) {
  const limiter = new SecurityRateLimiter();
  const clientIP = req.ip;
  const userId = req.session?.userId;

  // Check IP-based limits
  const ipLimit = await limiter.checkLimit('login_attempts', clientIP);
  if (!ipLimit.allowed) {
    return res.status(429).json({
      error: 'Too many requests',
      retryAfter: ipLimit.retryAfter
    });
  }

  // Check user-based limits if authenticated
  if (userId) {
    const userLimit = await limiter.checkLimit('user_login_attempts', userId);
    if (!userLimit.allowed) {
      return res.status(429).json({
        error: 'Too many login attempts',
        retryAfter: userLimit.retryAfter
      });
    }
  }

  next();
}
```

### Python

```python title="Advanced rate limiting" {11-16,28-33} "SecurityRateLimiter"

from typing import Dict, Optional

class SecurityRateLimiter:
    def __init__(self):
        self.limits = {
            # Per-IP limits
            'login_attempts': {'window': 900, 'max': 10},  # 10 attempts per 15 min
            'token_requests': {'window': 3600, 'max': 100},  # 100 requests per hour

            # Per-user limits
            'user_login_attempts': {'window': 3600, 'max': 5},  # 5 attempts per hour
            'user_token_refresh': {'window': 3600, 'max': 50},  # 50 refreshes per hour

            # Global limits
            'total_requests': {'window': 60, 'max': 10000}  # 10k requests per minute
        }

    async def check_limit(self, limit_type: str, identifier: str, custom_limit: Optional[Dict] = None):
        limit = custom_limit or self.limits.get(limit_type)
        if not limit:
            return {'allowed': True}

        key = f"{limit_type}:{identifier}"
        current = await redis.get(key) or 0
        current = int(current)

        if current >= limit['max']:
            await self.log_rate_limit_exceeded(limit_type, identifier, current)
            ttl = await redis.ttl(key)
            return {
                'allowed': False,
                'retry_after': ttl,
                'current': current,
                'max': limit['max']
            }

        # Increment counter with expiration
        pipeline = redis.pipeline()
        pipeline.incr(key)
        pipeline.expire(key, limit['window'])
        await pipeline.execute()

        return {'allowed': True, 'current': current + 1, 'max': limit['max']}

    # Dynamic rate limiting based on risk
    async def get_dynamic_limit(self, limit_type: str, risk_score: float):
        base_limit = self.limits[limit_type].copy()
        if risk_score > 0.8:
            base_limit['max'] = int(base_limit['max'] * 0.2)
        elif risk_score > 0.6:
            base_limit['max'] = int(base_limit['max'] * 0.5)
        return base_limit

# Rate limiting decorator
def rate_limit(limit_type: str):
    def decorator(func):
        async def wrapper(*args, **kwargs):
            request = kwargs.get('request') or args[0]
            limiter = SecurityRateLimiter()
            client_ip = request.client.host
            user_id = getattr(request.session, 'user_id', None)

            # Check IP-based limits
            ip_limit = await limiter.check_limit(limit_type, client_ip)
            if not ip_limit['allowed']:
                raise HTTPException(
                    status_code=429,
                    detail={
                        'error': 'Too many requests',
                        'retry_after': ip_limit['retry_after']
                    }
                )

            # Check user-based limits if authenticated
            if user_id:
                user_limit = await limiter.check_limit(f'user_{limit_type}', user_id)
                if not user_limit['allowed']:
                    raise HTTPException(
                        status_code=429,
                        detail={
                            'error': 'Too many attempts',
                            'retry_after': user_limit['retry_after']
                        }
                    )

            return await func(*args, **kwargs)
        return wrapper
    return decorator
```

### Go

```go title="Advanced rate limiting" {12-17,30-35} "SecurityRateLimiter"

	"context"
	"fmt"
	"time"
)

type RateLimit struct {
	Window time.Duration
	Max    int
}

type SecurityRateLimiter struct {
	limits map[string]RateLimit
	redis  RedisClient
}

func NewSecurityRateLimiter(redis RedisClient) *SecurityRateLimiter {
	return &SecurityRateLimiter{
		redis: redis,
		limits: map[string]RateLimit{
			// Per-IP limits
			"login_attempts":  {Window: 15 * time.Minute, Max: 10},
			"token_requests":  {Window: time.Hour, Max: 100},

			// Per-user limits
			"user_login_attempts": {Window: time.Hour, Max: 5},
			"user_token_refresh":  {Window: time.Hour, Max: 50},

			// Global limits
			"total_requests": {Window: time.Minute, Max: 10000},
		},
	}
}

type LimitResult struct {
	Allowed    bool
	RetryAfter int64
	Current    int
	Max        int
}

func (rl *SecurityRateLimiter) CheckLimit(ctx context.Context, limitType, identifier string, customLimit *RateLimit) (*LimitResult, error) {
	limit := customLimit
	if limit == nil {
		l, exists := rl.limits[limitType]
		if !exists {
			return &LimitResult{Allowed: true}, nil
		}
		limit = &l
	}

	key := fmt.Sprintf("%s:%s", limitType, identifier)
	current, err := rl.redis.Get(ctx, key).Int()
	if err != nil && err != redis.Nil {
		return nil, err
	}

	if current >= limit.Max {
		ttl, _ := rl.redis.TTL(ctx, key).Result()
		await rl.logRateLimitExceeded(limitType, identifier, current)
		return &LimitResult{
			Allowed:    false,
			RetryAfter: int64(ttl.Seconds()),
			Current:    current,
			Max:        limit.Max,
		}, nil
	}

	// Increment counter with expiration
	pipe := rl.redis.Pipeline()
	pipe.Incr(ctx, key)
	pipe.Expire(ctx, key, limit.Window)
	_, err = pipe.Exec(ctx)
	if err != nil {
		return nil, err
	}

	return &LimitResult{
		Allowed: true,
		Current: current + 1,
		Max:     limit.Max,
	}, nil
}

// Dynamic rate limiting based on risk
func (rl *SecurityRateLimiter) GetDynamicLimit(limitType string, riskScore float64) *RateLimit {
	baseLimit, exists := rl.limits[limitType]
	if !exists {
		return nil
	}

	if riskScore > 0.8 {
		return &RateLimit{
			Window: baseLimit.Window,
			Max:    int(float64(baseLimit.Max) * 0.2),
		}
	} else if riskScore > 0.6 {
		return &RateLimit{
			Window: baseLimit.Window,
			Max:    int(float64(baseLimit.Max) * 0.5),
		}
	}

	return &baseLimit
}

// Rate limiting middleware
func (rl *SecurityRateLimiter) RateLimitMiddleware(limitType string) gin.HandlerFunc {
	return func(c *gin.Context) {
		clientIP := c.ClientIP()
		userID, _ := c.Get("userID")

		// Check IP-based limits
		ipLimit, err := rl.CheckLimit(c.Request.Context(), limitType, clientIP, nil)
		if err != nil {
			c.JSON(500, gin.H{"error": "Internal server error"})
			c.Abort()
			return
		}

		if !ipLimit.Allowed {
			c.JSON(429, gin.H{
				"error":      "Too many requests",
				"retry_after": ipLimit.RetryAfter,
			})
			c.Abort()
			return
		}

		// Check user-based limits if authenticated
		if userID != nil {
			userLimit, err := rl.CheckLimit(c.Request.Context(), "user_"+limitType, userID.(string), nil)
			if err != nil {
				c.JSON(500, gin.H{"error": "Internal server error"})
				c.Abort()
				return
			}

			if !userLimit.Allowed {
				c.JSON(429, gin.H{
					"error":       "Too many attempts",
					"retry_after": userLimit.RetryAfter,
				})
				c.Abort()
				return
			}
		}

		c.Next()
	}
}
```

### Java

```java title="Advanced rate limiting" {13-18,32-37} "SecurityRateLimiter"

public class RateLimit {
    private final Duration window;
    private final int max;

    // constructors, getters...
}

@Component
public class SecurityRateLimiter {
    private final Map limits;
    private final RedisTemplate redisTemplate;

    public SecurityRateLimiter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.limits = Map.of(
            // Per-IP limits
            "login_attempts", new RateLimit(Duration.ofMinutes(15), 10),
            "token_requests", new RateLimit(Duration.ofHours(1), 100),

            // Per-user limits
            "user_login_attempts", new RateLimit(Duration.ofHours(1), 5),
            "user_token_refresh", new RateLimit(Duration.ofHours(1), 50),

            // Global limits
            "total_requests", new RateLimit(Duration.ofMinutes(1), 10000)
        );
    }

    public static class LimitResult {
        private final boolean allowed;
        private final long retryAfter;
        private final int current;
        private final int max;

        // constructors, getters...
    }

    public CompletableFuture checkLimit(String limitType, String identifier, RateLimit customLimit) {
        return CompletableFuture.supplyAsync(() -> {
            RateLimit limit = customLimit != null ? customLimit : limits.get(limitType);
            if (limit == null) {
                return new LimitResult(true, 0, 0, 0);
            }

            String key = limitType + ":" + identifier;
            String currentStr = redisTemplate.opsForValue().get(key);
            int current = currentStr != null ? Integer.parseInt(currentStr) : 0;

            if (current >= limit.getMax()) {
                Long ttl = redisTemplate.getExpire(key);
                logRateLimitExceeded(limitType, identifier, current);
                return new LimitResult(false, ttl, current, limit.getMax());
            }

            // Increment counter with expiration
            redisTemplate.opsForValue().increment(key);
            redisTemplate.expire(key, limit.getWindow());

            return new LimitResult(true, 0, current + 1, limit.getMax());
        });
    }

    // Dynamic rate limiting based on risk
    public RateLimit getDynamicLimit(String limitType, double riskScore) {
        RateLimit baseLimit = limits.get(limitType);
        if (baseLimit == null) return null;

        if (riskScore > 0.8) {
            return new RateLimit(baseLimit.getWindow(), (int) (baseLimit.getMax() * 0.2));
        } else if (riskScore > 0.6) {
            return new RateLimit(baseLimit.getWindow(), (int) (baseLimit.getMax() * 0.5));
        }

        return baseLimit;
    }
}

// Rate limiting interceptor
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    private final SecurityRateLimiter rateLimiter;

    public RateLimitInterceptor(SecurityRateLimiter rateLimiter) {
        this.rateLimiter = rateLimiter;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler) throws Exception {
        String clientIP = getClientIP(request);
        String userID = getUserID(request);

        // Check IP-based limits
        LimitResult ipLimit = rateLimiter.checkLimit("login_attempts", clientIP, null).get();
        if (!ipLimit.isAllowed()) {
            response.setStatus(429);
            response.getWriter().write(String.format(
                "{\"error\":\"Too many requests\",\"retry_after\":%d}",
                ipLimit.getRetryAfter()
            ));
            return false;
        }

        // Check user-based limits if authenticated
        if (userID != null) {
            LimitResult userLimit = rateLimiter.checkLimit("user_login_attempts", userID, null).get();
            if (!userLimit.isAllowed()) {
                response.setStatus(429);
                response.getWriter().write(String.format(
                    "{\"error\":\"Too many attempts\",\"retry_after\":%d}",
                    userLimit.getRetryAfter()
                ));
                return false;
            }
        }

        return true;
    }

    private String getClientIP(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }

    private String getUserID(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        return session != null ? (String) session.getAttribute("userID") : null;
    }
}
```

## Production security checklist

### Pre-deployment validation

1. **Environment security**
   - [ ] All secrets stored in secure environment variables
   - [ ] HTTPS enforced in production (no mixed content)
   - [ ] Security headers configured (HSTS, CSP, X-Frame-Options)
   - [ ] Database connections encrypted

2. **Authentication configuration**
   - [ ] Redirect URIs validated and restricted
   - [ ] Token lifetimes appropriate for security requirements
   - [ ] Refresh token rotation enabled
   - [ ] State parameter validation implemented

3. **Session management**
   - [ ] Secure session storage configured
   - [ ] Session timeout policies defined
   - [ ] Concurrent session limits set
   - [ ] Session invalidation on logout

4. **Rate limiting and monitoring**
   - [ ] Rate limiting configured for all auth endpoints
   - [ ] Security event logging implemented
   - [ ] Anomaly detection systems deployed
   - [ ] Alert systems configured

### Security testing procedures

Test security measures before production deployment:

```bash title="Security testing commands"
# OWASP ZAP security scan
zap-cli quick-scan --self-contained \
  --start-options '-config api.disablekey=true' \
  https://your-app.com

# SSL/TLS configuration test
testssl --full https://your-app.com

# CSRF protection test
curl -X POST https://your-app.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com"}'

# Rate limiting test
for i in {1..20}; do
  curl -X POST https://your-app.com/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"wrong"}'
done
```

### Incident response procedures

Define procedures for handling security incidents:

1. **Detection** - Automated alerts for suspicious activities
2. **Assessment** - Rapid impact evaluation and threat classification
3. **Containment** - Immediate actions to limit damage
4. **Investigation** - Forensic analysis and root cause identification
5. **Recovery** - System restoration and security improvements
6. **Communication** - Stakeholder notifications and compliance reporting

> caution: Security is an ongoing process
>
> Security implementation continues after deployment. Review and update security measures regularly, monitor for new threats, and maintain incident response capabilities.

### Production requirements

- **Use HTTPS** - Required in production for secure token transmission
- **Store tokens securely** - Use HTTP-only cookies or secure server-side storage
- **Validate redirects** - Configure allowed redirect URIs in your dashboard

This guide provides the foundation for implementing robust authentication security. Combine these patterns with regular security assessments and stay updated on emerging threats.


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
