Skip to content

Repository files navigation

SecretHound Authentication & Security

Overview

SecretHound supports two authentication methods:

  1. JWT (JSON Web Token) - For human users accessing the API via web/mobile apps
  2. API Keys - For machine-to-machine authentication (scanners, CI/CD integrations)

Users must register and login to receive a JWT bearer token. API keys can be created by authenticated users for programmatic access.


JWT Authentication (Human Users)

Authentication Flow

1. Registration

Endpoint: POST /api/auth/register

Request:

{
  "email": "user@example.com",
  "password": "SecurePassword123",
  "name": "John Doe"
}

Response (200 OK):

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "email": "user@example.com",
  "name": "John Doe",
  "role": "User"
}

What happens:

  • Password is hashed using Microsoft.AspNetCore.Identity.PasswordHasher<User> (PBKDF2 with HMAC-SHA256, 10,000 iterations by default)
  • User record is created in the database with hashed password
  • JWT token is generated and returned
  • User can immediately use the token for authenticated requests

2. Login

Endpoint: POST /api/auth/login

Request:

{
  "email": "user@example.com",
  "password": "SecurePassword123"
}

Response (200 OK):

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "email": "user@example.com",
  "name": "John Doe",
  "role": "User"
}

What happens:

  • User is fetched from database by email
  • Password is verified using PasswordHasher.VerifyHashedPassword()
  • If valid, JWT token is generated and returned
  • Returns 401 Unauthorized if credentials are invalid

3. Accessing Protected Endpoints

Example: POST /api/projects (requires authentication)

Request:

curl -X POST https://localhost:5001/api/projects \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"name": "My Project"}'

What happens:

  • JWT middleware validates the token signature using the secret key from appsettings.json
  • Token claims are extracted and user identity is established
  • If valid, request proceeds to controller
  • If invalid/missing, returns 401 Unauthorized

JWT Token Structure

Claims Included

Each JWT token contains the following claims:

Claim Type Description Example
sub Standard User ID (GUID) "3fa85f64-5717-4562-b3fc-2c963f66afa6"
email Standard User email address "user@example.com"
name Custom User display name "John Doe"
role Custom User role (User/Admin) "User" or "Admin"
jti Standard Unique token ID "a1b2c3d4-..."
iss Standard Token issuer "SecretHound"
aud Standard Token audience "SecretHoundClient"
exp Standard Expiration timestamp Unix timestamp

Token Lifetime

  • Access Token: 15 minutes (configurable via Jwt:AccessTokenLifetimeMinutes in appsettings.json)
  • No refresh tokens in current implementation (future enhancement)

Example Decoded Token

{
  "sub": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "email": "user@example.com",
  "name": "John Doe",
  "role": "User",
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "iss": "SecretHound",
  "aud": "SecretHoundClient",
  "exp": 1735001234
}

Password Security

Hashing Algorithm

  • Library: Microsoft.AspNetCore.Identity.PasswordHasher<User>
  • Algorithm: PBKDF2 with HMAC-SHA256
  • Iterations: 10,000 (default, configurable)
  • Salt: Unique per user, automatically generated
  • Output: Base64-encoded hash stored in User.PasswordHash field

Password Requirements

Current validation (enforced via RegisterRequest DTO):

  • Minimum length: 6 characters
  • Maximum length: 100 characters

Recommendation for production:

  • Increase minimum to 12+ characters
  • Require uppercase, lowercase, digit, and special character
  • Implement password complexity validation

Protected Endpoints

Current Protection

Endpoint Method Protected Required Role
/api/auth/register POST ❌ No -
/api/auth/login POST ❌ No -
/api/projects GET ❌ No -
/api/projects/{id} GET ❌ No -
/api/projects POST ✅ Yes Any authenticated user

Adding Protection to Other Endpoints

To protect an endpoint, add the [Authorize] attribute:

[HttpGet]
[Authorize] // Requires any authenticated user
public async Task<ActionResult<IEnumerable<ProjectDto>>> GetProjectsAsync()
{
    // ...
}

[HttpDelete("{id}")]
[Authorize(Roles = "Admin")] // Requires Admin role
public async Task<ActionResult> DeleteProjectAsync(Guid id)
{
    // ...
}

Configuration

JWT Settings (appsettings.json)

{
  "Jwt": {
    "Issuer": "SecretHound",
    "Audience": "SecretHoundClient",
    "AccessTokenLifetimeMinutes": 15,
    "RefreshTokenLifetimeDays": 7,
    "Secret": "super-secret-jwt-key-change-me"
  }
}

⚠️ IMPORTANT: Production Security

Before deploying to production:

  1. Change the JWT Secret:

    • Generate a strong random secret (minimum 32 characters)
    • Use environment variables or Azure Key Vault instead of appsettings.json
    # Example: Generate a secure secret
    openssl rand -base64 64
  2. Use Environment Variables:

    export Jwt__Secret="your-production-secret-here"
  3. Enable HTTPS Only:

    • Tokens should NEVER be transmitted over HTTP
    • Enforce HTTPS in production
  4. Implement Rate Limiting:

    • Protect /api/auth/login and /api/auth/register from brute force attacks
    • Consider using AspNetCoreRateLimit NuGet package
  5. Add Refresh Tokens:

    • Current implementation only uses short-lived access tokens
    • Implement refresh token rotation for better UX

Testing Authentication

Using Swagger UI

  1. Navigate to https://localhost:5001/swagger
  2. Click the "Authorize" button (top right)
  3. Enter token in format: Bearer <your-token>
  4. Click "Authorize"
  5. All subsequent requests will include the token

Using curl

# 1. Register
TOKEN=$(curl -s -X POST https://localhost:5001/api/auth/register \
  -H "Content-Type: application/json" \
  -k \
  -d '{
    "email": "test@example.com",
    "password": "TestPassword123",
    "name": "Test User"
  }' | jq -r '.token')

# 2. Use token to create project
curl -X POST https://localhost:5001/api/projects \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -k \
  -d '{"name": "My Project"}'

# 3. Try without token (should return 401)
curl -X POST https://localhost:5001/api/projects \
  -H "Content-Type: application/json" \
  -k \
  -d '{"name": "My Project"}'

User Roles

Available Roles

Defined in SecretHound.Domain.Enums.Role:

  • User (default for new registrations)
  • Admin

Role-Based Authorization

[Authorize(Roles = "Admin")]
public async Task<ActionResult> AdminOnlyEndpoint()
{
    // Only users with Admin role can access
}

[Authorize(Roles = "User,Admin")]
public async Task<ActionResult> UserOrAdminEndpoint()
{
    // Users with either User or Admin role can access
}

Accessing User Claims in Controllers

public async Task<ActionResult> SomeEndpoint()
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var email = User.FindFirst(ClaimTypes.Email)?.Value;
    var role = User.FindFirst(ClaimTypes.Role)?.Value;

    // Use claims for business logic
}

Security Best Practices

✅ Implemented

  • Password hashing with industry-standard algorithm (PBKDF2)
  • JWT signature validation
  • Token expiration enforcement
  • HTTPS redirection
  • Input validation on auth endpoints

⚠️ TODO for Production

  • Move JWT secret to environment variables/Key Vault
  • Implement refresh token rotation
  • Add rate limiting on auth endpoints
  • Implement account lockout after failed login attempts
  • Add email verification for new registrations
  • Implement password reset flow
  • Add audit logging for authentication events
  • Implement CORS restrictions (currently allows all origins)
  • Add multi-factor authentication (MFA)
  • Implement token revocation/blacklisting

API Key Authentication (Machine-to-Machine)

Overview

API keys provide a secure way for automated systems (scanners, CI/CD pipelines, integrations) to authenticate with the SecretHound API without requiring user credentials.

Key Features:

  • Keys are hashed using SHA-256 before storage (raw key never stored)
  • Only the key prefix is visible after creation
  • Keys are associated with a user account for authorization
  • Last used timestamp tracking
  • Can be activated/deactivated without deletion

Creating an API Key

Endpoint: POST /api/apikeys
Authentication: Requires JWT Bearer token

Request:

curl -X POST https://localhost:5001/api/apikeys \
  -H "Authorization: Bearer <your-jwt-token>" \
  -H "Content-Type: application/json" \
  -d '{"label": "CI/CD Scanner"}'

Response (200 OK):

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "apiKey": "sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "keyPrefix": "sk_a1b2c3d4e5",
  "label": "CI/CD Scanner",
  "createdAt": "2025-12-23T19:00:00Z",
  "warning": "This is the only time you will see this API key. Store it securely."
}

⚠️ IMPORTANT:

  • The apiKey field contains the raw API key and is only shown once
  • Store it immediately in a secure location (password manager, secrets vault)
  • The key cannot be retrieved again after this response
  • Only the keyPrefix (first 12 characters) is stored and shown in listings

Listing Your API Keys

Endpoint: GET /api/apikeys
Authentication: Requires JWT Bearer token

Request:

curl -X GET https://localhost:5001/api/apikeys \
  -H "Authorization: Bearer <your-jwt-token>"

Response (200 OK):

[
  {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "keyPrefix": "sk_a1b2c3d4e5",
    "label": "CI/CD Scanner",
    "createdAt": "2025-12-23T19:00:00Z",
    "lastUsedAt": "2025-12-23T20:15:00Z",
    "isActive": true
  },
  {
    "id": "7c8d9e0f-1234-5678-90ab-cdef12345678",
    "keyPrefix": "sk_x9y8z7w6v5",
    "label": "Development Scanner",
    "createdAt": "2025-12-20T10:00:00Z",
    "lastUsedAt": null,
    "isActive": true
  }
]

Note: Raw API keys are never returned in list responses, only the prefix.


Using an API Key

Header: X-API-Key: <your-api-key>

Example:

curl -X GET https://localhost:5001/api/projects \
  -H "X-API-Key: sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

How it works:

  1. Request includes X-API-Key header with the raw API key
  2. API key authentication handler hashes the provided key using SHA-256
  3. Hash is compared against stored hashes in the database
  4. If match found and key is active, user identity is established from associated user
  5. Request proceeds with user's permissions and role
  6. LastUsedAt timestamp is updated

Claims Available:

  • NameIdentifier - User ID associated with the API key
  • Name - User's display name
  • Email - User's email
  • Role - User's role (User/Admin)
  • ApiKeyId - The API key's unique ID
  • ApiKeyLabel - The API key's label

API Key Security

Storage

  • Raw key: Generated using cryptographically secure random bytes (32 bytes)
  • Format: sk_ prefix + 32 random alphanumeric characters
  • Hashing: SHA-256 hash of the full key is stored in database
  • Prefix: First 12 characters stored separately for display purposes

Best Practices

DO:

  • Store API keys in environment variables or secrets management systems
  • Use different API keys for different environments (dev/staging/prod)
  • Rotate API keys periodically
  • Use descriptive labels to identify key purpose
  • Revoke keys immediately if compromised
  • Monitor lastUsedAt for unexpected usage

DON'T:

  • Commit API keys to version control
  • Share API keys between services
  • Use the same key for multiple purposes
  • Store keys in plain text files
  • Log API keys in application logs

Authentication Scheme Selection

Endpoints can specify which authentication schemes they accept:

// JWT only (default for user-facing endpoints)
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<ActionResult> UserEndpoint() { }

// API Key only (for scanner/integration endpoints)
[Authorize(AuthenticationSchemes = "ApiKey")]
public async Task<ActionResult> ScannerEndpoint() { }

// Both JWT and API Key accepted
[Authorize(AuthenticationSchemes = "Bearer,ApiKey")]
public async Task<ActionResult> FlexibleEndpoint() { }

Current endpoint authentication:

Endpoint JWT API Key Notes
POST /api/auth/register Public
POST /api/auth/login Public
POST /api/apikeys JWT required to create keys
GET /api/apikeys JWT required to list keys
GET /api/projects Public (for now)
GET /api/projects/{id} Public (for now)
POST /api/projects Both schemes accepted

Complete Workflow Example

# 1. Register and login to get JWT
JWT=$(curl -s -X POST https://localhost:5001/api/auth/login \
  -H "Content-Type: application/json" \
  -k \
  -d '{"email":"user@example.com","password":"password123"}' \
  | jq -r '.token')

# 2. Create an API key using JWT
API_KEY_RESPONSE=$(curl -s -X POST https://localhost:5001/api/apikeys \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -k \
  -d '{"label":"My Scanner"}')

# 3. Extract the API key (SAVE THIS IMMEDIATELY)
API_KEY=$(echo $API_KEY_RESPONSE | jq -r '.apiKey')
echo "API Key: $API_KEY"
echo "WARNING: Save this key now! It won't be shown again."

# 4. Use the API key for subsequent requests
curl -X POST https://localhost:5001/api/projects \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -k \
  -d '{"name":"Scanned Project","description":"Created via API key"}'

# 5. List your API keys (using JWT, not API key)
curl -X GET https://localhost:5001/api/apikeys \
  -H "Authorization: Bearer $JWT" \
  -k

Troubleshooting

401 Unauthorized

Possible causes:

  • Token is missing from request
  • Token has expired (check exp claim)
  • Token signature is invalid (secret mismatch)
  • Token issuer/audience doesn't match configuration

Solution:

  • Ensure Authorization: Bearer <token> header is present
  • Login again to get a fresh token
  • Verify JWT settings match between token generation and validation

403 Forbidden

Possible causes:

  • User is authenticated but lacks required role
  • Endpoint requires Admin role but user has User role

Solution:

  • Check endpoint's [Authorize(Roles = "...")] attribute
  • Verify user's role in token claims

Token Validation Errors

Check logs for detailed error messages. Common issues:

  • Clock skew (token appears to be from the future)
  • Secret key mismatch between environments
  • Token format is malformed

References

About

A service that continuously scans code, configs, and container images for leaked secrets (API keys, tokens, passwords), gives a risk score per project, and provides remediation hints.

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages