Skip to main content

Overview

HGT EAM WebServices uses HTTP Basic Authentication to secure API access. All requests must include valid credentials that are validated against a configured list of authorized users. Additionally, the API enforces rate limiting (60 requests per minute per user) to prevent abuse and ensure fair resource allocation.
Basic Authentication transmits credentials in Base64 encoding. Always use HTTPS in production to prevent credential interception.

Authentication Flow

Basic Authentication Implementation

Configuration Setup

Credentials are configured in appsettings.json (or environment variables for production):
appsettings.json
{
  "EAMCredentials": [
    {
      "Username": "api_user_1",
      "Password": "SecureP@ssw0rd!",
      "Organization": "HGT"
    },
    {
      "Username": "readonly_user",
      "Password": "AnotherSecureP@ss",
      "Organization": "HGT_READONLY"
    }
  ]
}
Never commit passwords to source control. Use environment variables or secret management tools:
export EAMCredentials__0__Password="SecureP@ssw0rd!"
export EAMCredentials__1__Password="AnotherSecureP@ss"

Authentication Extension

The authentication system is configured in AuthorizationExtensions.cs:18-62:
AuthorizationExtensions.cs
public static IServiceCollection AddBasicAuthorization(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    // Load credentials from configuration
    var allCredentials = configuration
        .GetSection("EAMCredentials")
        .Get<List<EAMCredentialsSettings>>();
    
    if (allCredentials?.Count == 0)
        throw new InvalidOperationException("EAMCredentials section is empty");
    
    services.AddAuthentication(BasicDefaults.AuthenticationScheme)
        .AddBasic(options =>
        {
            options.Realm = "EAM-Webservices";
            options.Events = new BasicEvents
            {
                OnValidateCredentials = async context =>
                {
                    // Lookup user by username and password
                    var userInfo = allCredentials?.FirstOrDefault(f => 
                        f.Username == context.Username && 
                        f.Password == context.Password);
                    
                    if (userInfo != null)
                    {
                        // Create claims for authenticated user
                        var claims = new[]
                        {
                            new Claim(ClaimTypes.Name, userInfo.Username),
                            new Claim("Organization", userInfo.Organization),
                            new Claim("Password", userInfo.Password)
                        };
                        
                        context.Principal = new ClaimsPrincipal(
                            new ClaimsIdentity(claims, context.Scheme.Name));
                        context.Success();
                    }
                    else
                    {
                        context.ValidationFailed();
                    }
                }
            };
        });
    
    return services;
}

User Claims

After successful authentication, the system creates a ClaimsPrincipal with the following claims:
ClaimTypes.Name
string
The username of the authenticated user (e.g., api_user_1)
Organization
string
The EAM organization the user belongs to (e.g., HGT)
Password
string
The user’s password (stored in claims for passing to downstream EAM SOAP calls)
The password is stored in claims because the API acts as a proxy - it needs to pass credentials to the underlying EAM SOAP service on behalf of the user.

Making Authenticated Requests

Clients must include the Authorization header with Base64-encoded credentials:
curl -X GET "https://api.hgt-eam.com/api/accounting/payable?typeFilter=2&page=1" \
  -H "Authorization: Basic YXBpX3VzZXJfMTpTZWN1cmVQQHNzdzByZCE=" \
  -H "Accept: application/json"

Rate Limiting

Configuration

The API enforces 60 requests per minute per user, configured in Startup.cs:109-168:
Startup.cs
services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.ContentType = "application/json";
        
        var retryAfter = context.Lease.TryGetMetadata(
            MetadataName.RetryAfter, out var retryAfterValue)
            ? (int)Math.Ceiling(retryAfterValue.TotalSeconds)
            : (int?)null;
        
        if (retryAfter is not null)
        {
            context.HttpContext.Response.Headers.RetryAfter = 
                retryAfter.Value.ToString();
        }
        
        await context.HttpContext.Response.WriteAsync(
            $@"{{""statusCode"":429,""message"":""Too many requests"",
                ""retryAfterSeconds"":{(retryAfter ?? 0)}}}",
            token);
    };
    
    options.AddPolicy("api", httpContext =>
    {
        // Only apply to /api/* endpoints
        if (!httpContext.Request.Path.StartsWithSegments("/api"))
            return RateLimitPartition.GetNoLimiter("non-api");
        
        // Determine partition key (user or IP)
        string key;
        if (httpContext.User?.Identity?.IsAuthenticated == true)
        {
            key = $"user:{httpContext.User.Identity.Name}";
        }
        else
        {
            var ip = httpContext.Connection.RemoteIpAddress;
            key = ip is null ? "ip:unknown" : $"ip:{ip}";
        }
        
        // Fixed window: 60 requests per minute
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: key,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 60,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0,
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                AutoReplenishment = true
            });
    });
});

Rate Limit Behavior

Each authenticated user gets 60 requests per minute independently:
  • api_user_1: 60 req/min
  • readonly_user: 60 req/min (separate quota)
  • Unauthenticated requests: 60 req/min per IP address
The rate limiter uses a fixed window approach:
  • Window starts when first request arrives
  • Resets after 1 minute
  • No sliding window or token bucket
Example:
  • 12:00:00 - Request 1 (window starts)
  • 12:00:30 - Request 60 (quota exhausted)
  • 12:00:45 - Request 61 → 429 Too Many Requests
  • 12:01:00 - Window resets, quota replenished
When rate limit is exceeded, the API returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 15

{
  "statusCode": 429,
  "message": "Too many requests",
  "retryAfterSeconds": 15
}
The Retry-After header indicates seconds until quota resets.

Handling Rate Limits in Client Code

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    
    if (response.status !== 429) {
      return response;
    }
    
    // Parse retry-after header
    const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
    console.log(`Rate limited. Retrying after ${retryAfter}s...`);
    
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
  }
  
  throw new Error('Max retries exceeded');
}

// Usage
const response = await fetchWithRetry(
  'https://api.hgt-eam.com/api/accounting/payable',
  { headers: { 'Authorization': `Basic ${credentials}` } }
);

Security Best Practices

Basic Authentication encodes credentials in Base64, which is not encryption. Without HTTPS, credentials are transmitted in plain text.Configure HTTPS in production:
Startup.cs:45
app.UseHttpsRedirection();
app.UseHsts(); // Enforce HTTPS for 1 year
Check certificate configuration:
# Test HTTPS endpoint
curl -v https://api.hgt-eam.com/api/health

# Verify certificate validity
openssl s_client -connect api.hgt-eam.com:443 -servername api.hgt-eam.com
Never hardcode passwords in appsettings.json. Use environment variables or secret managers:
docker-compose.yml
services:
  api:
    image: hgt-eam-webservices:latest
    environment:
      - EAMCredentials__0__Username=api_user_1
      - EAMCredentials__0__Password=${API_USER_PASSWORD}
      - EAMCredentials__0__Organization=HGT
.env
API_USER_PASSWORD=SecureP@ssw0rd!
Implement a credential rotation policy:
  1. Create new credentials with a different username
  2. Add to configuration alongside existing credentials
  3. Update client applications to use new credentials
  4. Monitor usage of old credentials
  5. Remove old credentials after migration period
Example multi-credential setup:
{
  "EAMCredentials": [
    {
      "Username": "api_user_v2",  // New credentials
      "Password": "NewSecureP@ss!",
      "Organization": "HGT"
    },
    {
      "Username": "api_user_v1",  // Legacy, will be removed 2026-04-01
      "Password": "OldP@ssw0rd",
      "Organization": "HGT"
    }
  ]
}
The API logs authentication events via Serilog:
Startup.cs:62-69
using (LogContext.PushProperty("CurrentUser", user))
{
    Log.Information("Invoking endpoint {Method} {Path}", method, path);
}
Monitor for suspicious patterns:
  • Multiple 401 responses from same IP (brute force attempt)
  • 429 responses (rate limit abuse)
  • Successful auth followed by unusual request patterns
Example log query (Seq/Splunk):
StatusCode = 401 
| stats count by RemoteIpAddress 
| where count > 10
For high-security environments, restrict API access to known IP ranges:
Startup.cs (Add before authentication)
app.Use(async (context, next) =>
{
    var allowedIPs = new[] { "10.0.0.0/8", "192.168.1.0/24" };
    var remoteIP = context.Connection.RemoteIpAddress;
    
    if (!allowedIPs.Any(range => IsInRange(remoteIP, range)))
    {
        context.Response.StatusCode = 403;
        await context.Response.WriteAsync("Access denied");
        return;
    }
    
    await next();
});

Credential Flow to EAM Services

The API acts as a transparent proxy - user credentials are passed through to the underlying EAM SOAP service: Key Point: The API does not validate EAM credentials - it only checks if the username/password exist in the EAMCredentials configuration. The actual EAM authentication happens when calling the SOAP service.

Troubleshooting

Possible causes:
  1. Missing Authorization header
    # Missing header
    curl https://api.hgt-eam.com/api/accounting/payable
    # Result: 401 Unauthorized
    
  2. Incorrect credentials
    # Wrong password
    curl -u api_user:WRONG_PASSWORD https://api.hgt-eam.com/api/accounting/payable
    # Result: 401 Unauthorized
    
  3. Credentials not in configuration
    • Check appsettings.json or environment variables
    • Restart application after configuration changes
Solution:
# Verify credentials
echo -n "api_user:SecureP@ssw0rd!" | base64
# YXBpX3VzZXI6U2VjdXJlUEBzc3cwcmQh

curl -H "Authorization: Basic YXBpX3VzZXI6U2VjdXJlUEBzc3cwcmQh" \
     https://api.hgt-eam.com/api/accounting/payable
Cause: Exceeded 60 requests per minute quota.Solutions:
  1. Implement retry logic with exponential backoff (see code examples above)
  2. Reduce request frequency by caching responses client-side
  3. Use pagination efficiently - don’t request all pages at once
  4. Contact administrator if limit is insufficient for your use case
Check current usage:
# Monitor rate limit headers (if implemented)
curl -i https://api.hgt-eam.com/api/accounting/payable \
     -H "Authorization: Basic ..." \
     | grep -i "x-ratelimit"
If you receive 500 errors after successfully authenticating to the API:Cause: The EAM SOAP service rejected the credentials.Check logs:
docker logs hgt-eam-webservices | grep FaultException
Common scenarios:
  • EAM account is locked
  • EAM password has expired
  • Organization code is incorrect
  • EAM user lacks permissions for the requested grid
Solution: Verify EAM credentials by logging into EAM directly with the configured username/password.

Next Steps

Quick Start

Make your first authenticated API request

Architecture Overview

Understand how authentication fits into the system

API Reference

Explore available endpoints and parameters

Setup Guide

Configure authentication in production environments

Build docs developers (and LLMs) love