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