Skip to content

Security Features

GrydAuth includes enterprise-grade security features to protect your application against common threats.

Zero Trust Context Policies

Zero Trust now uses explicit validation contexts to avoid false positives and security drift:

  • PreAuthentication: used in unauthenticated flows such as POST /api/v1/auth/login and password reset request.
  • Authenticated: used in authenticated flows such as refresh token, switch tenant, and protected APIs.

Policy Scope

  • PreAuthentication runs only checks that are safe without a resolved user identity.
  • Authenticated runs full behavioral checks that depend on a known user/session.
  • Session integrity is explicitly skipped for bootstrap actions (login, login_postauth, switch_tenant) and required for token/session continuation actions (for example refresh_token and authenticated API requests).

This separation prevents login from using user-bound checks before user identity exists.

Policy Resolver (SOLID)

Zero Trust check orchestration is now centralized in a dedicated policy resolver:

  • IZeroTrustPolicyResolver
  • DefaultZeroTrustPolicyResolver

This removes rule branching from the validator internals and keeps policy decisions in one place (Context + Action), improving maintainability and reducing behavior drift across handlers/middleware.

Decision Model

  • Blocking is now driven by explicit blocking signals only (hard-block).
  • Risk score and soft signals produce step-up recommendation (RequiresAdditionalAuth) instead of automatic deny.
  • RiskThreshold is used for step-up escalation, not for blind request denial.
  • Login hard-block now returns code AUTH_RISK_FORBIDDEN (HTTP 403).

Threat Intelligence Promotion Rules

  • High-risk telemetry events are no longer enough by themselves to blacklist an IP.
  • Threat intel promotion now requires explicit event allow-list (for example: manual_ip_block, external_threat_feed_hit, credential_stuffing_detected) or explicit metadata override.
  • This prevents self-poisoning scenarios where internal validation telemetry blocks legitimate users.

Cache Isolation and TTL

  • Zero Trust cache state is now context-scoped (preauth vs auth) for threat intel and rate-limit dimensions.
  • Login-related threat intel keys use shorter retention (ThreatIntelPreAuth = 15 minutes).
  • Authenticated threat intel keeps standard retention (ThreatIntel = 1 hour).
  • This reduces cross-flow contamination and limits accidental lockout duration in pre-auth flows.

Canonical Security Session Store

Session integrity checks now use a dedicated store abstraction (ISecuritySessionStore) instead of ad-hoc cache key construction.

  • Canonical key: grydauth:security:session:{userId}:{sessionId}
  • Read/write/remove operations are centralized in SecuritySessionStore
  • Zero Trust session validation no longer depends on implicit string keys
  • Successful login persists a security session entry that is later consumed by authenticated zero-trust checks.
  • Middleware resolves session ID in this order: session_id claim, X-Security-Session-Id/X-Session-Id headers, then ASP.NET session ID fallback.

Operational Recovery Endpoint

To clear stale Zero Trust state without flushing Redis globally:

  • POST /api/v1/security/clear-state
  • Permission: admin:system
  • Request body:
json
{
  "ipAddress": "127.0.0.1",
  "userId": "11111111-1111-1111-1111-111111111111"
}

At least one of ipAddress or userId must be provided.

Observability Signals

Zero Trust now emits explicit decision telemetry:

  • zero_trust.context: PreAuthentication or Authenticated
  • zero_trust.decision: allow, step_up, blocked
  • zero_trust.violations_count
  • zero_trust.warnings_count
  • zero_trust.requires_step_up

Structured logs also capture threat-intel source and promotion decisions for forensic analysis.

Error Contract Mapping

Zero Trust handler failures now use centralized mapping (ZeroTrustFailureMapper):

  • Session-related blocking violations map to:
    • message key: Authentication.Session.InvalidOrExpired
    • code: SESSION_INVALIDATED (HTTP 401 via canonical error-code mapping)
  • Other zero-trust blocking violations keep their original operation-specific contract (for example AUTH_RISK_FORBIDDEN in login).

Frontend Consumption Rules

  • POST /api/v1/auth/login:
    • 403 + AUTH_RISK_FORBIDDEN => bloqueio por risco (sem logout automatico).
    • 401 + SESSION_INVALIDATED => sessao invalida (iniciar reautenticacao).
  • Endpoints autenticados:
    • 401 (SESSION_INVALIDATED, TOKEN_EXPIRED, TOKEN_INVALID, TOKEN_REVOKED) => limpar sessao local.
    • 403 (MISSING_PERMISSION, FORBIDDEN, TENANT_FORBIDDEN, CROSS_TENANT_FORBIDDEN) => manter sessao e exibir sem acesso.

Regression Coverage

Security regression tests now cover:

  • pre-auth vs post-auth zero-trust contexts in login flow
  • regression: login_postauth must not be blocked by missing pre-existing session
  • hard-block semantics vs soft-signal step-up semantics
  • anti self-poisoning rule for zero_trust_validation events
  • API contract for login risk blocking (403 + AUTH_RISK_FORBIDDEN)
  • middleware session ID propagation precedence (claim > header > ASP.NET session)

Rate Limiting

Protect endpoints against brute-force attacks and abuse.

Configuration

AddGrydAuth(configuration) registers native ASP.NET Core rate limiting and UseGrydAuth() adds it to the request pipeline. Configure the independent IP and normalized-account partitions in appsettings.json:

json
{
  "GrydAuth": {
    "Security": {
      "RateLimiting": {
        "Enabled": true,
        "AuthIpPermitLimit": 60,
        "AuthIpWindowSeconds": 60,
        "AuthAccountPermitLimit": 10,
        "AuthAccountWindowSeconds": 60,
        "MaxRequestsPerIpPerHour": 10,
        "MaxRequestsPerEmailPerHour": 3,
        "PasswordResetWindowSeconds": 3600,
        "QueueLimit": 0
      }
    }
  }
}

The built-in policies are auth-ip, auth-account, password-reset, and mfa. Rejections return 429, Retry-After, and the standard Gryd ProblemDetails contract.

Application rate limiting is defense in depth. Production deployments should also enforce coarse rate limits, connection limits, and bot protections at a trusted WAF or API gateway. Configure forwarded headers only for known proxies so the application receives the real client IP without trusting arbitrary X-Forwarded-For values.

Audit Logging

Track all security-related events.

Enable Audit Logging

csharp
builder.Services.AddGrydAuth(options =>
{
    options.AuditLogging.Enabled = true;
    options.AuditLogging.LogSuccessfulLogins = true;
    options.AuditLogging.LogFailedLogins = true;
    options.AuditLogging.LogPermissionChanges = true;
    options.AuditLogging.LogTokenOperations = true;
    options.AuditLogging.RetentionDays = 90;
});

Audit Events

EventDescription
UserLoginSuccessful login
UserLoginFailedFailed login attempt
UserLogoutUser logout
UserRegisteredNew user registration
PasswordChangedPassword change
PasswordResetRequestedPassword reset request
RoleAssignedRole assigned to user
RoleRevokedRole removed from user
PermissionGrantedPermission granted
PermissionRevokedPermission revoked
TokenRefreshedToken refreshed
TokenRevokedToken manually revoked
AccountLockedAccount locked due to failed attempts
AccountUnlockedAccount unlocked

Querying Audit Logs

http
GET /api/v1/audit-logs?userId=550e8400-e29b-41d4-a716-446655440000&eventType=UserLogin&from=2026-01-01&to=2026-01-31
Authorization: Bearer {admin-token}

Response:

json
{
  "items": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "userId": "550e8400-e29b-41d4-a716-446655440000",
      "eventType": "UserLogin",
      "ipAddress": "192.168.1.100",
      "userAgent": "Mozilla/5.0...",
      "timestamp": "2026-01-15T10:30:00Z",
      "success": true,
      "metadata": {
        "method": "Password",
        "mfaUsed": false
      }
    }
  ],
  "totalCount": 42,
  "page": 1,
  "pageSize": 20
}

Custom Audit Events

csharp
public class OrderService
{
    private readonly IAuditLogger _auditLogger;
    
    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        var order = await _orderRepository.CreateAsync(request);
        
        await _auditLogger.LogAsync(new AuditEvent
        {
            EventType = "OrderCreated",
            EntityType = "Order",
            EntityId = order.Id.ToString(),
            Metadata = new Dictionary<string, object>
            {
                ["amount"] = order.TotalAmount,
                ["items"] = order.Items.Count
            }
        });
        
        return order;
    }
}

Geo-Location Tracking

Track login locations and detect suspicious activity.

Configuration

csharp
builder.Services.AddGrydAuth(options =>
{
    options.GeoLocation.Enabled = true;
    options.GeoLocation.Provider = GeoLocationProvider.MaxMind;
    options.GeoLocation.BlockSuspiciousLogins = true;
    options.GeoLocation.TrustedCountries = ["US", "CA", "GB", "DE"];
});

MaxMind GeoIP Setup

csharp
builder.Services.AddMaxMindGeoIp(options =>
{
    options.AccountId = Configuration["MaxMind:AccountId"]!;
    options.LicenseKey = Configuration["MaxMind:LicenseKey"]!;
    options.DatabasePath = Path.Combine(AppContext.BaseDirectory, "GeoLite2-City.mmdb");
    options.AutoUpdate = true;
    options.UpdateIntervalDays = 7;
});

Location-Based Alerts

csharp
public class LocationSecurityHandler : ILoginEventHandler
{
    private readonly INotificationService _notifications;
    private readonly IGeoLocationService _geoLocation;
    
    public async Task HandleLoginAsync(LoginEvent loginEvent)
    {
        var currentLocation = await _geoLocation.GetLocationAsync(loginEvent.IpAddress);
        var lastLocation = await GetLastLoginLocationAsync(loginEvent.UserId);
        
        if (lastLocation != null && IsUnusualLocation(currentLocation, lastLocation))
        {
            await _notifications.SendAsync(
                loginEvent.UserId,
                "New Login Location",
                $"We detected a login from {currentLocation.City}, {currentLocation.Country}. " +
                "If this wasn't you, please change your password immediately."
            );
        }
    }
    
    private bool IsUnusualLocation(GeoLocation current, GeoLocation last)
    {
        // Check if locations are suspiciously far apart for the time elapsed
        var distance = CalculateDistance(current, last);
        var timeElapsed = current.Timestamp - last.Timestamp;
        var maxPossibleSpeed = 1000; // km/h (roughly commercial airline speed)
        
        return distance / timeElapsed.TotalHours > maxPossibleSpeed;
    }
}

Token Security

Asymmetric Signing & JWKS

Tokens are signed with RS256 (default) or ES256. The private key lives only in GrydAuth; verifiers use the public keys published at /.well-known/jwks.json. The validator pins ValidAlgorithms and resolves keys by kid, so HS256, alg:none and forged tokens are rejected with 401. This replaces the v4 shared-secret (HS256) model — see the v4 → v5 migration guide for keys, configuration and rotation.

Token Invalidation

csharp
// Invalidate specific token
await _tokenService.RevokeTokenAsync(refreshToken);

// Invalidate all user tokens
await _tokenService.RevokeAllUserTokensAsync(userId);

// Invalidate with reason
await _tokenService.RevokeTokenAsync(refreshToken, new RevocationReason
{
    Type = RevocationType.SecurityConcern,
    Description = "Suspicious activity detected"
});

Token Versioning

GrydAuth uses token versioning to invalidate tokens instantly:

csharp
public class User : AggregateRoot<Guid>
{
    public long TokenVersion { get; private set; } = 1;
    
    public void IncrementTokenVersion()
    {
        TokenVersion++;
        AddDomainEvent(new TokensInvalidatedEvent(Id, TokenVersion));
    }
}

All tokens issued before the current version are automatically rejected.

Revocation Identity (jti)

Revocation decisions use the token's full jti as the single identity across every code path — never a truncated token hash. Cached token data carries jti, iat and type, so a principal rebuilt from cache always re-runs the blacklist check. This closes the v4 gap where tokens served from cache could skip revocation (a logout/revocation bypass). No configuration is required.

Blacklist Caching

csharp
builder.Services.AddGrydAuth(options =>
{
    options.TokenBlacklist.Enabled = true;
    options.TokenBlacklist.CacheProvider = BlacklistCacheProvider.Redis;
    options.TokenBlacklist.CacheDuration = TimeSpan.FromDays(7);
});

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = Configuration.GetConnectionString("Redis");
    options.InstanceName = "GrydAuth:";
});

Cache Failure Policy (Fail-Secure)

When the security cache is unavailable, token validation is fail-secure by default: the token is rejected rather than accepted. This prevents a cache outage from re-enabling revoked or logged-out sessions, and matches the behavior of the token-version middleware.

jsonc
"GrydAuth": {
  "TokenValidation": {
    "CacheFailureMode": "FailSecure"  // default. "FailOpen" restores legacy v4 behavior (not recommended)
  }
}

Changed in v5

v4 was fail-open on the user-blacklist (Layer 3) check — revoked tokens could be accepted during a cache outage. v5 rejects them. Failures are logged distinguishing cache unavailability from genuine revocation, without leaking PII.

Security Headers

GrydAuth automatically adds security headers:

csharp
app.UseGrydAuth(); // Adds security middleware

// Headers added:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 1; mode=block
// Referrer-Policy: strict-origin-when-cross-origin
// Content-Security-Policy: default-src 'self'

Custom Security Headers

csharp
builder.Services.AddGrydAuth(options =>
{
    options.SecurityHeaders.EnableHSTS = true;
    options.SecurityHeaders.HSTSMaxAge = TimeSpan.FromDays(365);
    options.SecurityHeaders.IncludeSubDomains = true;
    options.SecurityHeaders.CustomHeaders = new Dictionary<string, string>
    {
        ["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
    };
});

Input Validation

Automatic Sanitization

csharp
builder.Services.AddGrydAuth(options =>
{
    options.InputValidation.SanitizeHtml = true;
    options.InputValidation.PreventSqlInjection = true;
    options.InputValidation.MaxRequestBodySize = 1024 * 1024; // 1MB
});

Request Validation

csharp
public class LoginRequestValidator : AbstractValidator<LoginRequest>
{
    public LoginRequestValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MaximumLength(256);
        
        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(8)
            .MaximumLength(128)
            .Must(NotContainSqlInjection).WithMessage("Invalid characters detected");
    }
    
    private bool NotContainSqlInjection(string password)
    {
        var sqlPatterns = new[] { "'", "--", ";", "/*", "*/", "xp_" };
        return !sqlPatterns.Any(p => password.Contains(p, StringComparison.OrdinalIgnoreCase));
    }
}

CORS Configuration

CORS is secure by default in v5. Use AddGrydAuthCors, which reads the allowed origins from configuration and denies cross-origin requests when no origins are set outside Development (logging a configuration error at startup). AllowCredentials is enabled only when explicit origins are present — there is no AllowAnyOrigin fallback.

jsonc
"GrydAuth": {
  "Cors": {
    "AllowedOrigins": [ "https://yourapp.com", "https://admin.yourapp.com" ]
  }
}
csharp
builder.Services.AddGrydAuthCors(builder.Configuration); // policy name: "GrydAuthCors"

app.UseCors("GrydAuthCors");

TIP

The legacy Cors:AllowedUrls (comma-separated) is still read as a fallback. In Development, common localhost origins are allowed automatically when nothing is configured. See the v4 → v5 migration guide.

Encryption

Data Encryption

csharp
builder.Services.AddGrydAuth(options =>
{
    options.Encryption.EncryptSensitiveData = true;
    options.Encryption.Algorithm = EncryptionAlgorithm.AES256;
    options.Encryption.KeyVaultUrl = Configuration["Azure:KeyVaultUrl"];
});

Password Hashing

GrydAuth uses Argon2id through Geralt/libsodium and stores passwords as versioned PHC strings. The hashing parameters are validated during startup:

json
{
  "GrydAuth": {
    "PasswordHashing": {
      "MemorySizeKiB": 19456,
      "Iterations": 2,
      "Parallelism": 1,
      "MaxConcurrency": 4
    }
  }
}

MemorySizeKiB, Iterations, and Parallelism cannot be configured below the security baseline (19456, 2, and 1). MaxConcurrency bounds simultaneous memory-hard operations and must be calibrated against the service memory limit and authentication SLA.

Increasing the memory or iteration parameters causes the next successful login to transparently replace the user's hash. A persistence failure does not reject an otherwise valid login.

Breaking migration

PBKDF2 hashes from versions before this change are intentionally unsupported. Before deployment, force a password reset for every existing local account and invalidate outstanding reset/session credentials according to the operational runbook. There is no legacy verification fallback.

Security Monitoring

Health Checks

csharp
builder.Services.AddHealthChecks()
    .AddGrydAuthHealthChecks();

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

Security Metrics

csharp
builder.Services.AddGrydAuth(options =>
{
    options.Metrics.Enabled = true;
    options.Metrics.Provider = MetricsProvider.Prometheus;
});

// Metrics exposed:
// gryd_auth_login_attempts_total
// gryd_auth_login_failures_total
// gryd_auth_tokens_issued_total
// gryd_auth_tokens_revoked_total
// gryd_auth_active_sessions_current

Security Checklist

Before going to production, ensure:

  • [ ] JWT secret is stored in secure vault (not appsettings.json)
  • [ ] HTTPS is enforced everywhere
  • [ ] Rate limiting is enabled for auth endpoints
  • [ ] Audit logging is enabled
  • [ ] Password policy meets requirements
  • [ ] Argon2id concurrency is calibrated for the container memory limit
  • [ ] Existing local users completed the mandatory password reset migration
  • [ ] Token expiration times are appropriate
  • [ ] CORS is properly configured
  • [ ] Security headers are enabled
  • [ ] Database connections are encrypted (SSL/TLS)
  • [ ] Sensitive data is encrypted at rest
  • [ ] Regular security audits are scheduled

Released under the MIT License.