Skip to content

Authentication Guide ​

Table of Contents ​

Overview ​

The Engagifii Events API uses OAuth 2.0 with the Client Credentials Grant flow for authentication. This is implemented through IdentityServer4, providing secure, token-based access to API resources.

Key Components ​

  1. Identity Server: Central authentication server that issues tokens
  2. Access Token: JWT token used to authenticate API requests
  3. Client Credentials: Your client ID and secret pair
  4. Tenant Code: Additional header for multi-tenant isolation

Authentication Flow ​

mermaid
sequenceDiagram
    participant Client
    participant IdentityServer
    participant EventsAPI
    
    Client->>IdentityServer: Request token (client_id, client_secret)
    IdentityServer->>Client: Access token (JWT)
    Client->>EventsAPI: API request + Bearer token + tenant-code
    EventsAPI->>Client: API response

OAuth 2.0 Implementation ​

Grant Type: Client Credentials ​

The Events API uses the OAuth 2.0 Client Credentials grant type, suitable for server-to-server authentication where no user context is required.

Token Endpoint ​

POST {IDENTITY_SERVER_URL}/connect/token

Required Parameters ​

ParameterDescriptionExample
client_idYour application's client IDevents_api_client
client_secretYour application's client secretxY9#mK2$pL8@nQ5
grant_typeMust be client_credentialsclient_credentials
scopeAPI scope, must include EventsApiEventsApi

Token Response ​

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE1NjkxMEQ4NDc3QUE0NzM2...",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "EventsApi"
}

Getting Access Tokens ​

Basic Token Request ​

bash
curl -X POST "{IDENTITY_SERVER_URL}/connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={CLIENT_ID}" \
  -d "client_secret={CLIENT_SECRET}" \
  -d "grant_type=client_credentials" \
  -d "scope=EventsApi"

Using the Access Token ​

Include the token in the Authorization header of all API requests:

bash
curl -X POST "https://engagifii-prod-event.azurewebsites.net/api/1.0/event/list" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -H "api-version: 1.0" \
  -H "tenant-code: {TENANT_CODE}" \
  -H "Content-Type: application/json" \
  -d '{}'

Required Headers for API Calls ​

Every API request must include these headers:

HeaderDescriptionExample
AuthorizationBearer token from Identity ServerBearer eyJhbGci...
api-versionAPI version to use1.0
tenant-codeYour organization's tenant codeTENANT123
Content-TypeRequest content typeapplication/json

Token Management ​

Token Lifecycle ​

  1. Token Request: Application requests token with credentials
  2. Token Issuance: Identity Server validates and issues token
  3. Token Usage: Token included in API requests
  4. Token Expiration: Default expiry is 3600 seconds (1 hour)
  5. Token Refresh: Request new token before expiration

Automatic Token Refresh Strategy ​

Implement proactive token refresh to avoid authentication failures:

javascript
class TokenManager {
  constructor(config) {
    this.config = config;
    this.token = null;
    this.tokenExpiry = null;
    this.refreshBuffer = 300; // Refresh 5 minutes before expiry
  }

  async getValidToken() {
    const now = Math.floor(Date.now() / 1000);
    
    // Check if token exists and is still valid
    if (this.token && this.tokenExpiry && (this.tokenExpiry - this.refreshBuffer) > now) {
      return this.token;
    }

    // Request new token
    return await this.refreshToken();
  }

  async refreshToken() {
    try {
      const response = await fetch(`${this.config.identityServerUrl}/connect/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: new URLSearchParams({
          client_id: this.config.clientId,
          client_secret: this.config.clientSecret,
          grant_type: 'client_credentials',
          scope: 'EventsApi'
        })
      });

      const data = await response.json();
      
      if (data.access_token) {
        this.token = data.access_token;
        this.tokenExpiry = Math.floor(Date.now() / 1000) + data.expires_in;
        return this.token;
      }
      
      throw new Error(data.error || 'Failed to obtain token');
    } catch (error) {
      console.error('Token refresh failed:', error);
      throw error;
    }
  }
}

Token Caching ​

Implement secure token caching to minimize authentication requests:

python
import time
import hashlib
from functools import lru_cache

class SecureTokenCache:
    def __init__(self):
        self._cache = {}
        
    def _get_cache_key(self, client_id):
        """Generate secure cache key"""
        return hashlib.sha256(client_id.encode()).hexdigest()
    
    def get(self, client_id):
        """Get cached token if valid"""
        key = self._get_cache_key(client_id)
        if key in self._cache:
            token_data = self._cache[key]
            if token_data['expiry'] > time.time():
                return token_data['token']
        return None
    
    def set(self, client_id, token, expires_in):
        """Cache token with expiration"""
        key = self._get_cache_key(client_id)
        self._cache[key] = {
            'token': token,
            'expiry': time.time() + expires_in - 300  # 5-minute buffer
        }
    
    def clear(self, client_id=None):
        """Clear cached tokens"""
        if client_id:
            key = self._get_cache_key(client_id)
            self._cache.pop(key, None)
        else:
            self._cache.clear()

Security Best Practices ​

1. Credential Storage ​

Never store credentials in:

  • Source code
  • Client-side applications
  • Version control systems
  • Unencrypted files

Secure storage options:

  • Environment variables
  • Secure key vaults (Azure Key Vault, AWS Secrets Manager)
  • Encrypted configuration files
  • Hardware security modules (HSM)

2. Token Handling ​

csharp
// C# Example: Secure token handling
public class SecureTokenHandler
{
    private readonly IConfiguration _configuration;
    private SecureString _token;
    
    public SecureTokenHandler(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    
    public async Task<string> GetTokenAsync()
    {
        // Get token from secure storage
        var token = await GetFromSecureStorageAsync();
        
        // Use SecureString for sensitive data
        _token = new SecureString();
        foreach (char c in token)
        {
            _token.AppendChar(c);
        }
        _token.MakeReadOnly();
        
        return Marshal.PtrToStringBSTR(Marshal.SecureStringToBSTR(_token));
    }
    
    private async Task<string> GetFromSecureStorageAsync()
    {
        // Retrieve from Azure Key Vault or similar
        var keyVaultUrl = _configuration["KeyVault:Url"];
        // Implementation details...
        return await SecureStorage.GetSecretAsync("api-token");
    }
}

3. Network Security ​

  • Always use HTTPS for all API communications
  • Implement certificate pinning for mobile applications
  • Use TLS 1.2 or higher
  • Validate SSL certificates
python
# Python: SSL certificate verification
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context

class SecureHTTPAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        context = create_urllib3_context()
        context.check_hostname = True
        context.verify_mode = ssl.CERT_REQUIRED
        kwargs['ssl_context'] = context
        return super().init_poolmanager(*args, **kwargs)

session = requests.Session()
session.mount('https://', SecureHTTPAdapter())

4. Rate Limiting and Throttling ​

Implement client-side rate limiting to prevent API abuse:

javascript
class RateLimiter {
  constructor(maxRequests = 100, windowMs = 60000) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = [];
  }

  async throttle() {
    const now = Date.now();
    
    // Remove old requests outside the window
    this.requests = this.requests.filter(time => now - time < this.windowMs);
    
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = this.requests[0];
      const waitTime = this.windowMs - (now - oldestRequest);
      
      if (waitTime > 0) {
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
    }
    
    this.requests.push(now);
  }
}

// Usage
const limiter = new RateLimiter(100, 60000); // 100 requests per minute

async function makeApiCall() {
  await limiter.throttle();
  // Make API call
}

5. Audit and Monitoring ​

Implement comprehensive logging for security monitoring:

csharp
public class SecurityAuditLogger
{
    private readonly ILogger<SecurityAuditLogger> _logger;
    
    public void LogAuthenticationAttempt(string clientId, bool success, string ipAddress)
    {
        var logEntry = new
        {
            EventType = "Authentication",
            ClientId = clientId,
            Success = success,
            IpAddress = ipAddress,
            Timestamp = DateTime.UtcNow,
            UserAgent = GetUserAgent()
        };
        
        if (success)
        {
            _logger.LogInformation("Authentication successful: {@LogEntry}", logEntry);
        }
        else
        {
            _logger.LogWarning("Authentication failed: {@LogEntry}", logEntry);
            
            // Trigger alerts for repeated failures
            CheckForBruteForceAttempt(clientId, ipAddress);
        }
    }
    
    private void CheckForBruteForceAttempt(string clientId, string ipAddress)
    {
        // Implement logic to detect and respond to brute force attempts
        // e.g., temporary IP blocking, alerting, etc.
    }
}

Code Examples ​

JavaScript/Node.js Complete Implementation ​

javascript
const axios = require('axios');
const NodeCache = require('node-cache');

class EngagifiiAuthClient {
  constructor(config) {
    this.config = config;
    this.tokenCache = new NodeCache({ stdTTL: 3300 }); // 55 minutes cache
    this.axiosInstance = this.createAxiosInstance();
  }

  createAxiosInstance() {
    const instance = axios.create({
      baseURL: this.config.apiBaseUrl,
      timeout: 30000,
      headers: {
        'api-version': this.config.apiVersion,
        'tenant-code': this.config.tenantCode
      }
    });

    // Add request interceptor for authentication
    instance.interceptors.request.use(
      async (config) => {
        const token = await this.getValidToken();
        config.headers['Authorization'] = `Bearer ${token}`;
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Add response interceptor for token refresh
    instance.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;

        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          
          // Clear cached token and retry
          this.tokenCache.del('access_token');
          const newToken = await this.getValidToken();
          originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
          
          return this.axiosInstance(originalRequest);
        }

        return Promise.reject(error);
      }
    );

    return instance;
  }

  async getValidToken() {
    // Check cache first
    const cachedToken = this.tokenCache.get('access_token');
    if (cachedToken) {
      return cachedToken;
    }

    // Request new token
    try {
      const response = await axios.post(
        `${this.config.identityServerUrl}/connect/token`,
        new URLSearchParams({
          client_id: this.config.clientId,
          client_secret: this.config.clientSecret,
          grant_type: 'client_credentials',
          scope: 'EventsApi'
        }),
        {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        }
      );

      const { access_token, expires_in } = response.data;
      
      // Cache token with buffer time
      this.tokenCache.set('access_token', access_token, expires_in - 300);
      
      return access_token;
    } catch (error) {
      console.error('Token acquisition failed:', error.response?.data || error.message);
      throw new Error('Authentication failed');
    }
  }

  // API methods using authenticated axios instance
  async listEvents(filter = {}) {
    const response = await this.axiosInstance.post('/api/1.0/event/list', filter);
    return response.data;
  }

  async getEvent(eventId) {
    const response = await this.axiosInstance.get(`/api/1.0/event/${eventId}`);
    return response.data;
  }
}

// Usage
const client = new EngagifiiAuthClient({
  apiBaseUrl: 'https://engagifii-prod-event.azurewebsites.net',
  identityServerUrl: process.env.IDENTITY_SERVER_URL,
  clientId: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  tenantCode: process.env.TENANT_CODE,
  apiVersion: '1.0'
});

// Make authenticated API calls
client.listEvents({ OnlyUpcoming: true })
  .then(events => console.log('Events:', events))
  .catch(error => console.error('Error:', error));

Python Complete Implementation ​

python
import requests
from datetime import datetime, timedelta
from threading import Lock
import logging
from typing import Optional, Dict, Any

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class EngagifiiAuthClient:
    def __init__(self, config: Dict[str, str]):
        self.config = config
        self.token: Optional[str] = None
        self.token_expiry: Optional[datetime] = None
        self.lock = Lock()
        self.session = requests.Session()
        self.session.headers.update({
            'api-version': config['api_version'],
            'tenant-code': config['tenant_code'],
            'Content-Type': 'application/json'
        })
    
    def _get_access_token(self) -> str:
        """Get a valid access token, refreshing if necessary."""
        with self.lock:
            # Check if current token is still valid
            if self.token and self.token_expiry and datetime.now() < self.token_expiry:
                return self.token
            
            # Request new token
            logger.info("Requesting new access token")
            
            try:
                response = requests.post(
                    f"{self.config['identity_server_url']}/connect/token",
                    data={
                        'client_id': self.config['client_id'],
                        'client_secret': self.config['client_secret'],
                        'grant_type': 'client_credentials',
                        'scope': 'EventsApi'
                    },
                    headers={'Content-Type': 'application/x-www-form-urlencoded'},
                    timeout=30
                )
                response.raise_for_status()
                
                token_data = response.json()
                self.token = token_data['access_token']
                
                # Set expiry with 5-minute buffer
                expires_in = token_data.get('expires_in', 3600)
                self.token_expiry = datetime.now() + timedelta(seconds=expires_in - 300)
                
                logger.info(f"Token acquired, expires at {self.token_expiry}")
                return self.token
                
            except requests.exceptions.RequestException as e:
                logger.error(f"Failed to acquire token: {e}")
                raise AuthenticationError(f"Token acquisition failed: {str(e)}")
    
    def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make an authenticated API request with automatic retry on 401."""
        url = f"{self.config['api_base_url']}{endpoint}"
        
        # First attempt
        token = self._get_access_token()
        self.session.headers['Authorization'] = f'Bearer {token}'
        
        response = self.session.request(method, url, **kwargs)
        
        # Handle token expiration
        if response.status_code == 401:
            logger.warning("Token expired, refreshing...")
            
            # Clear current token and get new one
            with self.lock:
                self.token = None
                self.token_expiry = None
            
            token = self._get_access_token()
            self.session.headers['Authorization'] = f'Bearer {token}'
            
            # Retry request
            response = self.session.request(method, url, **kwargs)
        
        response.raise_for_status()
        return response.json()
    
    def list_events(self, page_number: int = 1, page_size: int = 10, 
                   only_upcoming: bool = True) -> Dict[str, Any]:
        """List events with pagination."""
        return self._make_request(
            'POST',
            '/api/1.0/event/list',
            json={
                'PageNumber': page_number,
                'PageSize': page_size,
                'OnlyUpcoming': only_upcoming
            }
        )
    
    def get_event(self, event_id: str) -> Dict[str, Any]:
        """Get specific event details."""
        return self._make_request('GET', f'/api/1.0/event/{event_id}')

class AuthenticationError(Exception):
    """Raised when authentication fails."""
    pass

# Usage example
if __name__ == "__main__":
    import os
    from dotenv import load_dotenv
    
    load_dotenv()
    
    config = {
        'api_base_url': 'https://engagifii-prod-event.azurewebsites.net',
        'identity_server_url': os.getenv('IDENTITY_SERVER_URL'),
        'client_id': os.getenv('CLIENT_ID'),
        'client_secret': os.getenv('CLIENT_SECRET'),
        'tenant_code': os.getenv('TENANT_CODE'),
        'api_version': '1.0'
    }
    
    client = EngagifiiAuthClient(config)
    
    try:
        events = client.list_events()
        print(f"Found {events['totalCount']} events")
        
        for event in events['data']:
            print(f"- {event['name']} (ID: {event['id']})")
            
    except AuthenticationError as e:
        print(f"Authentication failed: {e}")
    except requests.exceptions.RequestException as e:
        print(f"API request failed: {e}")

C#/.NET Complete Implementation ​

csharp
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Polly;
using Polly.Extensions.Http;

public interface IEngagifiiAuthClient
{
    Task<T> GetAsync<T>(string endpoint);
    Task<T> PostAsync<T>(string endpoint, object data);
}

public class EngagifiiAuthClient : IEngagifiiAuthClient, IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly IMemoryCache _cache;
    private readonly ILogger<EngagifiiAuthClient> _logger;
    private readonly EngagifiiConfig _config;
    private readonly SemaphoreSlim _tokenSemaphore;
    private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;

    public EngagifiiAuthClient(
        HttpClient httpClient,
        IMemoryCache cache,
        ILogger<EngagifiiAuthClient> logger,
        EngagifiiConfig config)
    {
        _httpClient = httpClient;
        _cache = cache;
        _logger = logger;
        _config = config;
        _tokenSemaphore = new SemaphoreSlim(1, 1);
        
        // Configure retry policy with exponential backoff
        _retryPolicy = HttpPolicyExtensions
            .HandleTransientHttpError()
            .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            .WaitAndRetryAsync(
                3,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: async (outcome, timespan, retryCount, context) =>
                {
                    if (outcome.Result?.StatusCode == System.Net.HttpStatusCode.Unauthorized)
                    {
                        _logger.LogWarning("Token expired, refreshing...");
                        _cache.Remove("access_token");
                        await GetAccessTokenAsync();
                    }
                });
    }

    private async Task<string> GetAccessTokenAsync()
    {
        await _tokenSemaphore.WaitAsync();
        try
        {
            // Check cache
            if (_cache.TryGetValue("access_token", out string cachedToken))
            {
                return cachedToken;
            }

            _logger.LogInformation("Requesting new access token");

            var tokenRequest = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("client_id", _config.ClientId),
                new KeyValuePair<string, string>("client_secret", _config.ClientSecret),
                new KeyValuePair<string, string>("grant_type", "client_credentials"),
                new KeyValuePair<string, string>("scope", "EventsApi")
            });

            var tokenResponse = await _httpClient.PostAsync(
                $"{_config.IdentityServerUrl}/connect/token",
                tokenRequest
            );

            tokenResponse.EnsureSuccessStatusCode();

            var tokenContent = await tokenResponse.Content.ReadAsStringAsync();
            dynamic tokenData = JsonConvert.DeserializeObject(tokenContent);

            string accessToken = tokenData.access_token;
            int expiresIn = tokenData.expires_in;

            // Cache token with buffer
            var cacheOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(expiresIn - 300)
            };
            
            _cache.Set("access_token", accessToken, cacheOptions);
            
            _logger.LogInformation($"Token acquired, expires in {expiresIn} seconds");
            
            return accessToken;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to acquire access token");
            throw new AuthenticationException("Token acquisition failed", ex);
        }
        finally
        {
            _tokenSemaphore.Release();
        }
    }

    private async Task<HttpRequestMessage> PrepareRequestAsync(
        HttpMethod method, 
        string endpoint, 
        object data = null)
    {
        var token = await GetAccessTokenAsync();
        
        var request = new HttpRequestMessage(method, 
            $"{_config.ApiBaseUrl}{endpoint}");
        
        request.Headers.Add("Authorization", $"Bearer {token}");
        request.Headers.Add("api-version", _config.ApiVersion);
        request.Headers.Add("tenant-code", _config.TenantCode);

        if (data != null)
        {
            var json = JsonConvert.SerializeObject(data);
            request.Content = new StringContent(json, 
                System.Text.Encoding.UTF8, 
                "application/json");
        }

        return request;
    }

    public async Task<T> GetAsync<T>(string endpoint)
    {
        var request = await PrepareRequestAsync(HttpMethod.Get, endpoint);
        
        var response = await _retryPolicy.ExecuteAsync(async () => 
            await _httpClient.SendAsync(request)
        );

        response.EnsureSuccessStatusCode();
        
        var content = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(content);
    }

    public async Task<T> PostAsync<T>(string endpoint, object data)
    {
        var request = await PrepareRequestAsync(HttpMethod.Post, endpoint, data);
        
        var response = await _retryPolicy.ExecuteAsync(async () => 
            await _httpClient.SendAsync(request)
        );

        response.EnsureSuccessStatusCode();
        
        var content = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(content);
    }

    public void Dispose()
    {
        _tokenSemaphore?.Dispose();
    }
}

// Dependency Injection Configuration
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Configure HttpClient
        services.AddHttpClient<IEngagifiiAuthClient, EngagifiiAuthClient>()
            .ConfigureHttpClient(client =>
            {
                client.Timeout = TimeSpan.FromSeconds(30);
            })
            .AddPolicyHandler(GetRetryPolicy());

        // Add memory cache
        services.AddMemoryCache();

        // Configure settings
        services.AddSingleton<EngagifiiConfig>(provider =>
        {
            var configuration = provider.GetRequiredService<IConfiguration>();
            return configuration.GetSection("Engagifii").Get<EngagifiiConfig>();
        });
    }

    private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
    {
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(
                3,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    Console.WriteLine($"Retry {retryCount} after {timespan} seconds");
                });
    }
}

// Usage
public class EventService
{
    private readonly IEngagifiiAuthClient _client;

    public EventService(IEngagifiiAuthClient client)
    {
        _client = client;
    }

    public async Task<EventListResponse> GetEventsAsync()
    {
        return await _client.PostAsync<EventListResponse>(
            "/api/1.0/event/list",
            new { PageNumber = 1, PageSize = 10, OnlyUpcoming = true }
        );
    }

    public async Task<EventViewModel> GetEventAsync(Guid eventId)
    {
        return await _client.GetAsync<EventViewModel>($"/api/1.0/event/{eventId}");
    }
}

Error Scenarios ​

Common Authentication Errors ​

1. Invalid Client Credentials ​

Response:

json
{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}

Causes:

  • Incorrect client_id or client_secret
  • Client credentials expired or revoked
  • Client not authorized for the requested scope

Resolution:

  • Verify credentials with your administrator
  • Ensure credentials are properly encoded
  • Check client is authorized for EventsApi scope

2. Invalid Scope ​

Response:

json
{
  "error": "invalid_scope",
  "error_description": "The requested scope is invalid, unknown, or malformed"
}

Resolution:

  • Ensure scope includes "EventsApi"
  • Check for typos in scope parameter
  • Verify client is authorized for requested scope

3. Expired Token ​

Response:

json
{
  "error": "invalid_token",
  "error_description": "The access token expired"
}

Resolution:

  • Implement automatic token refresh
  • Check token expiry before making requests
  • Handle 401 responses with token refresh

4. Missing Tenant Code ​

Response:

json
{
  "error": {
    "code": "MISSING_TENANT",
    "message": "Tenant code header is required"
  }
}

Resolution:

  • Include tenant-code header in all requests
  • Verify tenant code is correct
  • Check header name is exactly "tenant-code"

Troubleshooting ​

Debug Authentication Issues ​

Enable detailed logging to troubleshoot authentication:

javascript
// JavaScript debugging
const debug = require('debug')('engagifii:auth');

class DebugAuthClient {
  async getToken() {
    debug('Requesting token with client_id:', this.config.clientId);
    
    try {
      const response = await this.requestToken();
      debug('Token received, expires in:', response.expires_in);
      return response.access_token;
    } catch (error) {
      debug('Token request failed:', error);
      throw error;
    }
  }
}

Common Issues Checklist ​

  1. Token not refreshing:

    • Check system time synchronization
    • Verify refresh logic triggers before expiry
    • Ensure thread-safe token refresh
  2. Intermittent 401 errors:

    • Implement retry logic with exponential backoff
    • Check for race conditions in token refresh
    • Verify token cache implementation
  3. SSL/TLS errors:

    • Update to latest TLS libraries
    • Verify certificate chain is complete
    • Check proxy/firewall settings
  4. Performance issues:

    • Implement token caching
    • Reuse HTTP connections
    • Monitor token refresh frequency

Health Check Endpoint ​

Implement a health check to verify authentication:

python
async def health_check(client):
    """Verify authentication and API connectivity"""
    try:
        # Test token acquisition
        token = await client.get_valid_token()
        assert token is not None, "No token received"
        
        # Test API call
        response = await client.list_events(page_size=1)
        assert 'data' in response, "Invalid API response"
        
        return {
            'status': 'healthy',
            'authentication': 'ok',
            'api_connectivity': 'ok',
            'timestamp': datetime.now().isoformat()
        }
    except Exception as e:
        return {
            'status': 'unhealthy',
            'error': str(e),
            'timestamp': datetime.now().isoformat()
        }

Additional Resources ​