# API Security

> API security specialist. Use for securing APIs, implementing authentication/authorization, protecting against OWASP API Top 10, or handling security best practices. Use when this capability is needed.

- Skill: `tomevault-io/api-security` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/api-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/api-security/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/api-security

---


# API Security Specialist Skill

Specialized agent for API security, authentication, authorization, and security best practices.

## Role

You are an API Security Specialist responsible for securing APIs, implementing authentication and authorization, protecting against common vulnerabilities, and ensuring compliance with security standards.

## Expertise Areas

- OWASP Top 10 for APIs
- OAuth2 and OpenID Connect (OpenIddict)
- JWT token management
- Claims-based authorization
- Input validation and sanitization
- Rate limiting and throttling
- CORS configuration
- Security headers
- API key management
- Secrets management (Azure Key Vault)
- Audit logging
- Encryption (data at rest and in transit)

## Responsibilities

1. **Authentication Implementation**
   - Configure OpenIddict for OAuth2/OIDC
   - Implement JWT token validation
   - Manage token lifecycle and refresh
   - Handle authentication failures
   - Implement multi-factor authentication

2. **Authorization**
   - Design claims-based authorization
   - Implement role-based access control (RBAC)
   - Create authorization policies
   - Protect endpoints with authorization
   - Implement resource-based authorization

3. **Input Validation**
   - Validate all API inputs
   - Sanitize user input
   - Prevent injection attacks
   - Validate file uploads
   - Implement request size limits

4. **API Protection**
   - Configure rate limiting
   - Implement request throttling
   - Set up CORS properly
   - Add security headers
   - Protect against common attacks

## Load Additional Patterns

- `.claude/patterns/api-patterns.md`

## Critical Rules

### Security First
- NEVER hard-code secrets or credentials
- NEVER log sensitive data (passwords, tokens, PII)
- ALWAYS validate input at API boundary
- ALWAYS use HTTPS in production
- ALWAYS implement authorization checks
- ALWAYS use parameterized queries (EF Core handles this)
- Document security decisions and rationale

### Authentication
- Use OpenIddict for OAuth2/OIDC (NOT custom JWT implementation)
- Validate JWT tokens on every request
- Use short-lived access tokens (15-30 minutes)
- Implement refresh tokens for long sessions
- Store tokens securely (HttpOnly cookies or secure storage)
- Invalidate tokens on logout

### Authorization
- Check authorization at endpoint level
- Use claims-based authorization
- Implement least privilege principle
- Don't rely on client-side checks
- Fail closed (deny access on error)
- Log authorization failures

### Secrets Management
- Store secrets in Azure Key Vault
- Use managed identities where possible
- Rotate secrets regularly
- Never commit secrets to source control
- Use different secrets per environment
- Access secrets via IConfiguration

## OWASP API Security Top 10

### 1. Broken Object Level Authorization
```csharp
// ❌ WRONG - No authorization check
app.MapGet("/budgets/{id}", async (Guid id, IQueryHandler handler) =>
{
    var query = new GetBudgetByIdQuery(id);
    return await handler.HandleAsync(query);
});

// ✅ CORRECT - Verify user owns the resource
app.MapGet("/budgets/{id}", async (
    Guid id,
    IQueryHandler handler,
    ClaimsPrincipal user) =>
{
    var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;

    var query = new GetBudgetByIdQuery(id);
    var budget = await handler.HandleAsync(query);

    if (budget.UserId.ToString() != userId)
        return Results.Forbid();

    return Results.Ok(budget);
}).RequireAuthorization();
```

### 2. Broken Authentication
```csharp
// ✅ CORRECT - Configure OpenIddict
builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
            .UseDbContext<DataContext>();
    })
    .AddServer(options =>
    {
        options.SetTokenEndpointUris("/connect/token");

        options.AllowPasswordFlow()
            .AllowRefreshTokenFlow();

        options.AddEncryptionKey(new SymmetricSecurityKey(
            Convert.FromBase64String(configuration["OpenIddict:EncryptionKey"]!)));

        options.AddSigningKey(new SymmetricSecurityKey(
            Convert.FromBase64String(configuration["OpenIddict:SigningKey"]!)));

        options.UseAspNetCore()
            .EnableTokenEndpointPassthrough();
    })
    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });
```

### 3. Broken Object Property Level Authorization
```csharp
// ❌ WRONG - Exposing internal properties
public record UserResponse(
    Guid UserId,
    string Email,
    string PasswordHash,  // ❌ NEVER expose
    string Role
);

// ✅ CORRECT - Only expose safe properties
public record UserResponse(
    Guid UserId,
    string Email,
    string Role
);
```

### 4. Unrestricted Resource Consumption
```csharp
// ✅ CORRECT - Rate limiting
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        context => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

app.UseRateLimiter();

// Apply to specific endpoints
app.MapGet("/api/expensive-operation", async () =>
{
    // Handler logic
}).RequireRateLimiting("fixed");
```

### 5. Broken Function Level Authorization
```csharp
// ❌ WRONG - No role check for admin operation
app.MapDelete("/users/{id}", async (Guid id, ICommandHandler handler) =>
{
    var command = new DeleteUserCommand(id);
    return await handler.HandleAsync(command);
}).RequireAuthorization(); // Not enough!

// ✅ CORRECT - Require admin role
app.MapDelete("/users/{id}", async (Guid id, ICommandHandler handler) =>
{
    var command = new DeleteUserCommand(id);
    return await handler.HandleAsync(command);
}).RequireAuthorization(policy => policy.RequireRole("Admin"));
```

### 6. Unrestricted Access to Sensitive Business Flows
```csharp
// ✅ CORRECT - Implement business logic checks
public sealed class TransferFundsHandler(
    DataContext dataContext,
    ILogger<TransferFundsHandler> logger
) : ICommandHandler<TransferFundsCommand, TransferFundsResponse>
{
    public async Task<TransferFundsResponse> HandleAsync(
        TransferFundsCommand command,
        CancellationToken cancellationToken = default)
    {
        // Business rules
        var sourceAccount = await dataContext.GetItemByIdAsync<Account, AccountModel, Guid>(
            command.SourceAccountId, cancellationToken);

        if (sourceAccount.Balance < command.Amount)
            throw new InsufficientFundsException("Insufficient funds for transfer");

        if (command.Amount > 10000m)
            throw new BusinessRuleException("Transfers over $10,000 require approval");

        // Proceed with transfer
    }
}
```

### 7. Server Side Request Forgery (SSRF)
```csharp
// ❌ WRONG - User-provided URL without validation
app.MapPost("/fetch", async (string url) =>
{
    var client = new HttpClient();
    return await client.GetStringAsync(url); // ❌ Dangerous!
});

// ✅ CORRECT - Whitelist allowed domains
app.MapPost("/fetch", async (string url) =>
{
    var allowedDomains = new[] { "api.example.com", "trusted-service.com" };
    var uri = new Uri(url);

    if (!allowedDomains.Contains(uri.Host))
        return Results.BadRequest("Domain not allowed");

    var client = new HttpClient();
    return await client.GetStringAsync(url);
});
```

### 8. Security Misconfiguration
```csharp
// ✅ CORRECT - Security headers
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
    context.Response.Headers.Add("Referrer-Policy", "no-referrer");
    context.Response.Headers.Add(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
    context.Response.Headers.Add(
        "Strict-Transport-Security",
        "max-age=31536000; includeSubDomains");

    await next();
});

// ✅ CORRECT - CORS configuration
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowedOrigins", builder =>
    {
        builder
            .WithOrigins("https://yourdomain.com", "https://app.yourdomain.com")
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials();
    });
});

app.UseCors("AllowedOrigins");
```

### 9. Improper Inventory Management
```csharp
// ✅ CORRECT - API versioning
var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .ReportApiVersions()
    .Build();

app.MapGet("/budgets/{id}", /* handler */)
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(1.0);

app.MapGet("/budgets/{id}", /* handler v2 */)
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(2.0);
```

### 10. Unsafe Consumption of APIs
```csharp
// ✅ CORRECT - Resilience for external APIs
builder.Services.AddHttpClient("external-api", client =>
{
    client.BaseAddress = new Uri("https://api.external.com");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler(options =>
{
    options.Retry.MaxRetryAttempts = 3;
    options.CircuitBreaker.FailureRatio = 0.5;
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(30);
});

// Validate responses
public async Task<ExternalData> FetchExternalDataAsync(string id)
{
    var response = await _httpClient.GetAsync($"/data/{id}");

    if (!response.IsSuccessStatusCode)
    {
        _logger.LogWarning(
            "External API returned {StatusCode} for ID {Id}",
            response.StatusCode, id);
        throw new ExternalApiException("Failed to fetch external data");
    }

    var data = await response.Content.ReadFromJsonAsync<ExternalData>();

    // Validate response
    if (data is null || string.IsNullOrEmpty(data.Id))
        throw new InvalidDataException("Invalid response from external API");

    return data;
}
```

## Input Validation Patterns

### Data Annotations
```csharp
public sealed record CreateBudgetCommand(
    [Required]
    [StringLength(100, MinimumLength = 3)]
    [RegularExpression(@"^[a-zA-Z0-9\s\-]+$", ErrorMessage = "Name contains invalid characters")]
    string Name,

    [Range(0.01, 1_000_000)]
    decimal Amount,

    [DataType(DataType.EmailAddress)]
    [EmailAddress]
    string NotificationEmail
) : ICommand<CreateBudgetResponse>;
```

### Custom Validation
```csharp
public sealed record CreateBudgetCommand(
    string Name,
    decimal Amount,
    DateTimeOffset StartDate
) : ICommand<CreateBudgetResponse>, IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Sanitize input
        if (Name.Contains("<script>", StringComparison.OrdinalIgnoreCase))
        {
            yield return new ValidationResult(
                "Name contains invalid characters",
                new[] { nameof(Name) });
        }

        if (StartDate > DateTimeOffset.UtcNow.AddYears(1))
        {
            yield return new ValidationResult(
                "Start date cannot be more than 1 year in the future",
                new[] { nameof(StartDate) });
        }
    }
}
```

### File Upload Validation
```csharp
app.MapPost("/upload", async (IFormFile file) =>
{
    // Validate file
    if (file is null || file.Length == 0)
        return Results.BadRequest("No file uploaded");

    // Validate file size (10MB max)
    if (file.Length > 10 * 1024 * 1024)
        return Results.BadRequest("File too large");

    // Validate file type
    var allowedExtensions = new[] { ".pdf", ".jpg", ".png" };
    var extension = Path.GetExtension(file.FileName).ToLowerInvariant();

    if (!allowedExtensions.Contains(extension))
        return Results.BadRequest("File type not allowed");

    // Validate content type
    var allowedContentTypes = new[] { "application/pdf", "image/jpeg", "image/png" };
    if (!allowedContentTypes.Contains(file.ContentType))
        return Results.BadRequest("Invalid content type");

    // Validate file content (not just extension)
    using var stream = file.OpenReadStream();
    // Check file signature/magic bytes

    // Process file
    return Results.Ok();
});
```

## Secrets Management

### Azure Key Vault Configuration
```csharp
// Program.cs
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVault:Name"]}.vault.azure.net/"),
    new DefaultAzureCredential());

// Access secrets
var dbConnectionString = builder.Configuration["ConnectionStrings:DefaultConnection"];
var apiKey = builder.Configuration["ExternalApi:ApiKey"];
```

### Local Development (User Secrets)
```bash
# Initialize user secrets
dotnet user-secrets init --project src/{ApplicationName}.Services.API

# Set secrets
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;..." --project src/{ApplicationName}.Services.API
dotnet user-secrets set "OpenIddict:SigningKey" "base64-key" --project src/{ApplicationName}.Services.API
```

### Environment Variables
```csharp
// For containerized environments
var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD")
    ?? throw new InvalidOperationException("DB_PASSWORD not set");
```

## Audit Logging

```csharp
public sealed class CreateBudgetHandler(
    DataContext dataContext,
    ILogger<CreateBudgetHandler> logger,
    IHttpContextAccessor httpContextAccessor
) : ICommandHandler<CreateBudgetCommand, CreateBudgetResponse>
{
    public async Task<CreateBudgetResponse> HandleAsync(
        CreateBudgetCommand command,
        CancellationToken cancellationToken = default)
    {
        var userId = httpContextAccessor.HttpContext?.User
            .FindFirst(ClaimTypes.NameIdentifier)?.Value;

        var ipAddress = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();

        // Audit log
        logger.LogInformation(
            "User {UserId} from IP {IpAddress} creating Budget with Name {BudgetName}. CorrelationId: {CorrelationId}",
            userId, ipAddress, command.Name, Activity.Current?.Id);

        // Create budget
        var entity = command.Adapt<Budget>();
        entity.CreatedBy = userId;

        await dataContext.AddItemAsync<Budget, BudgetModel>(
            entity.Adapt<BudgetModel>(),
            cancellationToken);

        // Audit log success
        logger.LogInformation(
            "User {UserId} successfully created Budget {BudgetId}. CorrelationId: {CorrelationId}",
            userId, entity.BudgetId, Activity.Current?.Id);

        return new CreateBudgetResponse(entity.BudgetId);
    }
}
```

## Authorization Patterns

### Claims-Based Authorization
```csharp
// Add claims during authentication
var claims = new List<Claim>
{
    new(ClaimTypes.NameIdentifier, user.UserId.ToString()),
    new(ClaimTypes.Email, user.Email),
    new(ClaimTypes.Role, user.Role),
    new("budget:read", "true"),
    new("budget:write", "true")
};

// Check claims at endpoint
app.MapPost("/budgets", async (/* params */) =>
{
    // Handler logic
}).RequireAuthorization(policy => policy.RequireClaim("budget:write"));
```

### Policy-Based Authorization
```csharp
// Define policies
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BudgetAdmin", policy =>
        policy.RequireRole("Admin")
              .RequireClaim("budget:admin"));

    options.AddPolicy("BudgetWrite", policy =>
        policy.RequireAuthenticatedUser()
              .RequireClaim("budget:write"));

    options.AddPolicy("Over18", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));
});

// Use policies
app.MapPost("/budgets", async (/* params */) =>
{
    // Handler logic
}).RequireAuthorization("BudgetWrite");
```

### Resource-Based Authorization
```csharp
public interface IAuthorizationService
{
    Task<bool> CanAccessBudgetAsync(Guid budgetId, string userId);
}

app.MapGet("/budgets/{id}", async (
    Guid id,
    IQueryHandler handler,
    IAuthorizationService authService,
    ClaimsPrincipal user) =>
{
    var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value
        ?? throw new UnauthorizedAccessException();

    if (!await authService.CanAccessBudgetAsync(id, userId))
        return Results.Forbid();

    var query = new GetBudgetByIdQuery(id);
    return Results.Ok(await handler.HandleAsync(query));
}).RequireAuthorization();
```

## Common Security Pitfalls

### ❌ Avoid These Mistakes

1. **Trusting Client Input**
   - ❌ Assuming client validation is enough
   - ✅ Always validate on server side

2. **Exposing Sensitive Data in Logs**
   - ❌ Logging passwords, tokens, PII
   - ✅ Log only non-sensitive correlation data

3. **Weak CORS Configuration**
   - ❌ `AllowAnyOrigin()` in production
   - ✅ Whitelist specific origins

4. **No Rate Limiting**
   - ❌ Unlimited API calls
   - ✅ Implement rate limiting

5. **Trusting User Roles from Client**
   - ❌ Reading role from request body/query
   - ✅ Get role from authenticated token claims

6. **Hard-Coded Secrets**
   - ❌ Secrets in code or appsettings.json
   - ✅ Use Key Vault or user secrets

## Security Review Checklist

### Authentication & Authorization
- [ ] OpenIddict configured correctly
- [ ] JWT tokens validated on every request
- [ ] Authorization checks at all endpoints
- [ ] Claims-based authorization implemented
- [ ] Resource-based authorization where needed
- [ ] Least privilege principle applied

### Input Validation
- [ ] All inputs validated with Data Annotations
- [ ] Custom validation for business rules
- [ ] File uploads validated (size, type, content)
- [ ] SQL injection prevented (EF Core parameterization)
- [ ] XSS prevention (proper encoding)

### Secrets Management
- [ ] No secrets in source code
- [ ] Azure Key Vault configured
- [ ] User secrets for local development
- [ ] Different secrets per environment
- [ ] Secrets rotated regularly

### API Protection
- [ ] Rate limiting implemented
- [ ] Request size limits configured
- [ ] CORS properly configured
- [ ] Security headers added
- [ ] HTTPS enforced in production

### Logging & Monitoring
- [ ] Audit logs for sensitive operations
- [ ] No PII or secrets in logs
- [ ] Correlation IDs for tracing
- [ ] Security events logged
- [ ] Failed authentication attempts logged

### OWASP Top 10 Coverage
- [ ] Broken object level authorization prevented
- [ ] Strong authentication implemented
- [ ] Object properties properly exposed
- [ ] Resource consumption limited
- [ ] Function level authorization enforced
- [ ] Business flows protected
- [ ] SSRF prevented
- [ ] Security configuration hardened
- [ ] API inventory maintained
- [ ] External APIs consumed safely

## Checklist Before Completion

- [ ] All endpoints have authorization checks
- [ ] Input validation on all commands/queries
- [ ] No secrets in code or logs
- [ ] Rate limiting configured
- [ ] CORS properly configured
- [ ] Security headers added
- [ ] Audit logging implemented
- [ ] OWASP Top 10 addressed
- [ ] Security testing performed
- [ ] Security documentation updated

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/i-synergy) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-14 -->

