mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Splitting code into multiple files with reasonable testing coverage.
```
ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements
ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements
ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements
github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements
ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements
ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements
ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements
ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements
ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements
ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements
ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements
ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements
ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements
````
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
// Package validators provides validation functionality for session data
|
||||
package validators
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxBrowserCookieSize = 3500
|
||||
maxCookieSize = 1200
|
||||
)
|
||||
|
||||
// SessionValidator provides validation operations for session data
|
||||
type SessionValidator struct{}
|
||||
|
||||
// NewSessionValidator creates a new session validator
|
||||
func NewSessionValidator() *SessionValidator {
|
||||
return &SessionValidator{}
|
||||
}
|
||||
|
||||
// ValidateChunkSize checks if a chunk will fit within browser cookie limits.
|
||||
// It estimates the encoded size including cookie overhead and headers
|
||||
// to ensure the chunk won't exceed browser-imposed cookie size limits.
|
||||
func (sv *SessionValidator) ValidateChunkSize(chunkData string) bool {
|
||||
estimatedEncodedSize := len(chunkData) + (len(chunkData)*50)/100
|
||||
return estimatedEncodedSize <= maxBrowserCookieSize
|
||||
}
|
||||
|
||||
// IsCorruptionMarker detects if data contains known corruption indicators.
|
||||
// It checks for specific corruption markers and invalid characters
|
||||
// that indicate the data has been tampered with or corrupted.
|
||||
func (sv *SessionValidator) IsCorruptionMarker(data string) bool {
|
||||
if data == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
corruptionMarkers := []string{
|
||||
"__CORRUPTION_MARKER_TEST__",
|
||||
"__INVALID_BASE64_DATA__",
|
||||
"__CORRUPTED_CHUNK_DATA__",
|
||||
"!@#$%^&*()",
|
||||
"<<<CORRUPTED>>>",
|
||||
}
|
||||
|
||||
for _, marker := range corruptionMarkers {
|
||||
if data == marker {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) > 10 {
|
||||
invalidChars := "!@#$%^&*(){}[]|\\:;\"'<>?,`~"
|
||||
for _, char := range invalidChars {
|
||||
if strings.ContainsRune(data, char) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateTokenFormat validates that a token has the correct JWT format
|
||||
func (sv *SessionValidator) ValidateTokenFormat(token, tokenType string) error {
|
||||
if token == "" {
|
||||
return nil // Empty token is not an error
|
||||
}
|
||||
|
||||
// JWT tokens should have exactly 3 parts separated by dots
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return &ValidationError{
|
||||
Type: tokenType,
|
||||
Reason: "invalid JWT format",
|
||||
Details: "token must have exactly 3 parts separated by dots",
|
||||
}
|
||||
}
|
||||
|
||||
// Each part should be non-empty
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
return &ValidationError{
|
||||
Type: tokenType,
|
||||
Reason: "empty token part",
|
||||
Details: strings.Join([]string{"token part", string(rune(i + 1)), "is empty"}, " "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSessionIntegrity performs comprehensive validation of session data integrity
|
||||
func (sv *SessionValidator) ValidateSessionIntegrity(sessionData SessionData) error {
|
||||
if sessionData == nil {
|
||||
return &ValidationError{
|
||||
Type: "session",
|
||||
Reason: "nil session data",
|
||||
Details: "session data cannot be nil",
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication state consistency
|
||||
authenticated := sessionData.GetAuthenticated()
|
||||
email := sessionData.GetEmail()
|
||||
|
||||
if authenticated && email == "" {
|
||||
return &ValidationError{
|
||||
Type: "session",
|
||||
Reason: "authentication inconsistency",
|
||||
Details: "session is authenticated but has no email",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate token formats if present
|
||||
if accessToken := sessionData.GetAccessToken(); accessToken != "" {
|
||||
if err := sv.ValidateTokenFormat(accessToken, "access"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if idToken := sessionData.GetIDToken(); idToken != "" {
|
||||
if err := sv.ValidateTokenFormat(idToken, "id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if refreshToken := sessionData.GetRefreshToken(); refreshToken != "" {
|
||||
// Refresh tokens don't have to be JWTs, so we do basic validation
|
||||
if len(refreshToken) == 0 {
|
||||
return &ValidationError{
|
||||
Type: "refresh",
|
||||
Reason: "empty refresh token",
|
||||
Details: "refresh token cannot be empty if set",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSessionTiming validates session timing and expiration
|
||||
func (sv *SessionValidator) ValidateSessionTiming(sessionData SessionData, maxAge time.Duration) error {
|
||||
if sessionData == nil {
|
||||
return &ValidationError{
|
||||
Type: "session",
|
||||
Reason: "nil session data",
|
||||
Details: "session data cannot be nil",
|
||||
}
|
||||
}
|
||||
|
||||
// Check refresh token timing
|
||||
refreshTokenIssuedAt := sessionData.GetRefreshTokenIssuedAt()
|
||||
if !refreshTokenIssuedAt.IsZero() {
|
||||
age := time.Since(refreshTokenIssuedAt)
|
||||
if age > maxAge {
|
||||
return &ValidationError{
|
||||
Type: "timing",
|
||||
Reason: "refresh token expired",
|
||||
Details: strings.Join([]string{"refresh token age", age.String(), "exceeds max age", maxAge.String()}, " "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateEmailDomain validates that an email belongs to an allowed domain
|
||||
func (sv *SessionValidator) ValidateEmailDomain(email string, allowedDomains map[string]struct{}) error {
|
||||
if email == "" {
|
||||
return &ValidationError{
|
||||
Type: "email",
|
||||
Reason: "empty email",
|
||||
Details: "email cannot be empty",
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowedDomains) == 0 {
|
||||
return nil // No domain restrictions
|
||||
}
|
||||
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return &ValidationError{
|
||||
Type: "email",
|
||||
Reason: "invalid email format",
|
||||
Details: "email must contain exactly one @ symbol",
|
||||
}
|
||||
}
|
||||
|
||||
domain := parts[1]
|
||||
if _, allowed := allowedDomains[domain]; !allowed {
|
||||
return &ValidationError{
|
||||
Type: "email",
|
||||
Reason: "domain not allowed",
|
||||
Details: strings.Join([]string{"domain", domain, "is not in allowed domains list"}, " "),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SplitIntoChunks splits a string into chunks that fit within cookie size limits
|
||||
func (sv *SessionValidator) SplitIntoChunks(s string, chunkSize int) []string {
|
||||
effectiveChunkSize := min(chunkSize, maxCookieSize)
|
||||
|
||||
var chunks []string
|
||||
for len(s) > 0 {
|
||||
if len(s) > effectiveChunkSize {
|
||||
chunks = append(chunks, s[:effectiveChunkSize])
|
||||
s = s[effectiveChunkSize:]
|
||||
} else {
|
||||
chunks = append(chunks, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
// ValidateChunks validates all chunks in a chunk set
|
||||
func (sv *SessionValidator) ValidateChunks(chunks []string) error {
|
||||
for i, chunk := range chunks {
|
||||
if chunk == "" {
|
||||
return &ValidationError{
|
||||
Type: "chunk",
|
||||
Reason: "empty chunk",
|
||||
Details: strings.Join([]string{"chunk", string(rune(i)), "is empty"}, " "),
|
||||
}
|
||||
}
|
||||
|
||||
if !sv.ValidateChunkSize(chunk) {
|
||||
return &ValidationError{
|
||||
Type: "chunk",
|
||||
Reason: "chunk too large",
|
||||
Details: strings.Join([]string{"chunk", string(rune(i)), "exceeds size limit"}, " "),
|
||||
}
|
||||
}
|
||||
|
||||
if sv.IsCorruptionMarker(chunk) {
|
||||
return &ValidationError{
|
||||
Type: "chunk",
|
||||
Reason: "corrupted chunk",
|
||||
Details: strings.Join([]string{"chunk", string(rune(i)), "contains corruption markers"}, " "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidationError represents a validation error with context
|
||||
type ValidationError struct {
|
||||
Type string
|
||||
Reason string
|
||||
Details string
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (ve *ValidationError) Error() string {
|
||||
return strings.Join([]string{ve.Type, "validation error:", ve.Reason, "-", ve.Details}, " ")
|
||||
}
|
||||
|
||||
// SessionData interface for validation operations
|
||||
type SessionData interface {
|
||||
GetAuthenticated() bool
|
||||
GetEmail() string
|
||||
GetAccessToken() string
|
||||
GetIDToken() string
|
||||
GetRefreshToken() string
|
||||
GetRefreshTokenIssuedAt() time.Time
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
// min returns the minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ValidateChunkSize is a package-level function for backward compatibility
|
||||
func ValidateChunkSize(chunkData string) bool {
|
||||
sv := &SessionValidator{}
|
||||
return sv.ValidateChunkSize(chunkData)
|
||||
}
|
||||
|
||||
// IsCorruptionMarker is a package-level function for backward compatibility
|
||||
func IsCorruptionMarker(data string) bool {
|
||||
sv := &SessionValidator{}
|
||||
return sv.IsCorruptionMarker(data)
|
||||
}
|
||||
|
||||
// SplitIntoChunks is a package-level function for backward compatibility
|
||||
func SplitIntoChunks(s string, chunkSize int) []string {
|
||||
sv := &SessionValidator{}
|
||||
return sv.SplitIntoChunks(s, chunkSize)
|
||||
}
|
||||
Reference in New Issue
Block a user