Skip to content

Authentication & Security Guide

Table of Contents

Overview

The Engagifii Training & Accreditation API implements a multi-layered security architecture designed to protect sensitive training and certification data. The system uses tenant-based isolation with optional JWT bearer token authentication for enhanced security.

Security Architecture

┌─────────────────┐
│   Client App    │
└────────┬────────┘

    HTTPS Only

┌────────▼────────┐
│   API Gateway   │ ◄── Rate Limiting
└────────┬────────┘

  Tenant Validation

┌────────▼────────┐
│ Authentication  │ ◄── Token Validation (Optional)
└────────┬────────┘

   Authorization

┌────────▼────────┐
│  API Endpoint   │
└─────────────────┘

Authentication Methods

1. Tenant-Based Authentication (Required)

Every API request must include a tenant code to identify and isolate your organization's data.

Implementation

http
GET /api/v1/Awards/List HTTP/1.1
Host: engagifii-trainingandaccreditation.azurewebsites.net
tenant-code: YOUR_TENANT_CODE
Content-Type: application/json

Tenant Code Properties

  • Format: Alphanumeric string (e.g., "org-12345", "acme-corp")
  • Case Sensitive: Yes
  • Required: For all API endpoints
  • Location: HTTP Header
  • Header Name: tenant-code

2. Bearer Token Authentication (Optional)

For organizations requiring additional security, JWT bearer tokens can be implemented.

Token Format

http
GET /api/v1/Awards/List HTTP/1.1
Host: engagifii-trainingandaccreditation.azurewebsites.net
tenant-code: YOUR_TENANT_CODE
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

3. API Key Authentication (Contact Administrator)

Some organizations may use API keys for service-to-service communication.

http
GET /api/v1/Awards/List HTTP/1.1
Host: engagifii-trainingandaccreditation.azurewebsites.net
tenant-code: YOUR_TENANT_CODE
X-API-Key: YOUR_API_KEY
Content-Type: application/json

Token Acquisition

OAuth 2.0 Flow (If Enabled)

For organizations using OAuth 2.0, follow this flow to obtain access tokens:

1. Authorization Code Flow

javascript
// Step 1: Redirect user to authorization endpoint
const authUrl = `https://auth.engagifii.com/authorize?
  response_type=code&
  client_id=${CLIENT_ID}&
  redirect_uri=${REDIRECT_URI}&
  scope=AccreditationAPI&
  state=${STATE}`;

window.location.href = authUrl;

// Step 2: Exchange authorization code for token
async function exchangeCodeForToken(authCode) {
  const response = await fetch('https://auth.engagifii.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: authCode,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: REDIRECT_URI
    })
  });
  
  return await response.json();
}

2. Client Credentials Flow (Service Accounts)

javascript
async function getServiceToken() {
  const response = await fetch('https://auth.engagifii.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      scope: 'AccreditationAPI'
    })
  });
  
  const data = await response.json();
  return data.access_token;
}

Token Response Format

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "8xLQqz5QXoP...",
  "scope": "AccreditationAPI"
}

Token Management

Storage Best Practices

Browser Applications

javascript
// Use sessionStorage for temporary storage
class TokenManager {
  static setToken(token) {
    sessionStorage.setItem('api_token', token);
    sessionStorage.setItem('token_expires', Date.now() + (3600 * 1000));
  }
  
  static getToken() {
    const expires = sessionStorage.getItem('token_expires');
    if (!expires || Date.now() > parseInt(expires)) {
      this.clearToken();
      return null;
    }
    return sessionStorage.getItem('api_token');
  }
  
  static clearToken() {
    sessionStorage.removeItem('api_token');
    sessionStorage.removeItem('token_expires');
  }
}

Server Applications

python
import time
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

class TokenManager:
    def __init__(self):
        self.token = None
        self.expires_at = 0
    
    def set_token(self, token_data):
        self.token = token_data['access_token']
        self.expires_at = time.time() + token_data['expires_in'] - 60  # 60s buffer
    
    def get_token(self):
        if time.time() >= self.expires_at:
            self.refresh_token()
        return self.token
    
    def refresh_token(self):
        # Implement token refresh logic
        pass
    
    def is_token_valid(self):
        if not self.token:
            return False
        
        try:
            # Decode without verification for expiry check
            decoded = jwt.decode(self.token, options={"verify_signature": False})
            return decoded['exp'] > time.time()
        except:
            return False

Token Refresh Strategy

javascript
class ApiClient {
  constructor(config) {
    this.config = config;
    this.token = null;
    this.refreshPromise = null;
  }
  
  async getValidToken() {
    // If token is valid, return it
    if (this.token && this.isTokenValid()) {
      return this.token;
    }
    
    // If refresh is in progress, wait for it
    if (this.refreshPromise) {
      return await this.refreshPromise;
    }
    
    // Start refresh process
    this.refreshPromise = this.refreshToken();
    this.token = await this.refreshPromise;
    this.refreshPromise = null;
    
    return this.token;
  }
  
  async refreshToken() {
    const response = await fetch('https://auth.engagifii.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: this.config.refreshToken,
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret
      })
    });
    
    const data = await response.json();
    this.config.refreshToken = data.refresh_token;
    
    return data.access_token;
  }
  
  isTokenValid() {
    if (!this.token) return false;
    
    const payload = JSON.parse(atob(this.token.split('.')[1]));
    return payload.exp > (Date.now() / 1000) + 60; // 60s buffer
  }
  
  async apiCall(endpoint, options = {}) {
    const token = await this.getValidToken();
    
    return fetch(`${this.config.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'tenant-code': this.config.tenantCode,
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
  }
}

Security Best Practices

1. HTTPS Requirements

  • Always use HTTPS: Never send credentials over HTTP
  • Certificate Validation: Verify SSL certificates
  • TLS Version: Use TLS 1.2 or higher
javascript
// Node.js example with strict SSL
const https = require('https');
const agent = new https.Agent({
  rejectUnauthorized: true, // Reject invalid certificates
  minVersion: 'TLSv1.2'
});

2. Token Security

Never expose tokens in:

  • URL parameters
  • Browser console logs
  • Client-side code repositories
  • Error messages

Secure token handling:

javascript
// BAD - Token in URL
fetch(`${BASE_URL}/api/v1/Awards/List?token=${token}`);

// GOOD - Token in header
fetch(`${BASE_URL}/api/v1/Awards/List`, {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

// BAD - Logging tokens
console.log('Token:', token);

// GOOD - Log token status only
console.log('Token valid:', !!token);

3. Rate Limiting

Implement client-side rate limiting to avoid hitting API limits:

javascript
class RateLimiter {
  constructor(maxRequests = 10, windowMs = 1000) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = [];
  }
  
  async throttle() {
    const now = Date.now();
    this.requests = this.requests.filter(time => now - time < this.windowMs);
    
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = this.requests[0];
      const delay = this.windowMs - (now - oldestRequest);
      await new Promise(resolve => setTimeout(resolve, delay));
      return this.throttle();
    }
    
    this.requests.push(now);
  }
}

const limiter = new RateLimiter(10, 1000);

async function apiCallWithRateLimit(endpoint, options) {
  await limiter.throttle();
  return apiCall(endpoint, options);
}

4. Input Validation

Always validate and sanitize input before sending to API:

javascript
class InputValidator {
  static validateTenantCode(code) {
    if (!code || typeof code !== 'string') {
      throw new Error('Invalid tenant code');
    }
    
    // Remove any potential injection attempts
    return code.replace(/[^a-zA-Z0-9-_]/g, '');
  }
  
  static validateGuid(guid) {
    const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    if (!guidRegex.test(guid)) {
      throw new Error('Invalid GUID format');
    }
    return guid.toLowerCase();
  }
  
  static sanitizeInput(input) {
    if (typeof input === 'string') {
      // Remove any HTML/Script tags
      return input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
                  .replace(/<[^>]+>/g, '');
    }
    return input;
  }
}

5. Secure Error Handling

Never expose sensitive information in error messages:

javascript
class SecureErrorHandler {
  static handle(error, context) {
    // Log full error internally
    console.error('API Error:', {
      timestamp: new Date().toISOString(),
      context: context,
      error: error.message,
      stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
    });
    
    // Return sanitized error to client
    if (error.status === 401) {
      return {
        error: 'Authentication failed',
        code: 'AUTH_FAILED',
        action: 'Please check your credentials'
      };
    }
    
    if (error.status === 403) {
      return {
        error: 'Access denied',
        code: 'ACCESS_DENIED',
        action: 'You do not have permission for this action'
      };
    }
    
    // Generic error for unexpected issues
    return {
      error: 'An error occurred',
      code: 'GENERAL_ERROR',
      action: 'Please try again or contact support'
    };
  }
}

Authentication Examples

JavaScript (Node.js) Complete Example

javascript
const axios = require('axios');

class EngagifiiApiClient {
  constructor(config) {
    this.baseUrl = 'https://engagifii-trainingandaccreditation.azurewebsites.net';
    this.tenantCode = config.tenantCode;
    this.clientId = config.clientId;
    this.clientSecret = config.clientSecret;
    this.token = null;
    this.tokenExpires = 0;
    
    // Configure axios instance
    this.client = axios.create({
      baseURL: this.baseUrl,
      timeout: 30000,
      headers: {
        'tenant-code': this.tenantCode
      }
    });
    
    // Add request interceptor for authentication
    this.client.interceptors.request.use(
      async (config) => {
        const token = await this.getToken();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
    
    // Add response interceptor for error handling
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;
        
        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          this.token = null;
          const token = await this.getToken();
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return this.client(originalRequest);
        }
        
        return Promise.reject(error);
      }
    );
  }
  
  async getToken() {
    if (this.token && Date.now() < this.tokenExpires) {
      return this.token;
    }
    
    try {
      const response = await axios.post('https://auth.engagifii.com/token', 
        new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: this.clientId,
          client_secret: this.clientSecret,
          scope: 'AccreditationAPI'
        }), {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }
      );
      
      this.token = response.data.access_token;
      this.tokenExpires = Date.now() + (response.data.expires_in * 1000) - 60000;
      
      return this.token;
    } catch (error) {
      console.error('Failed to get token:', error.message);
      throw error;
    }
  }
  
  async getAwards() {
    const response = await this.client.get('/api/v1/Awards/List');
    return response.data;
  }
  
  async createClassRegistration(registrationData) {
    const response = await this.client.post(
      '/api/v1/registration/ClassRegistration',
      registrationData
    );
    return response.data;
  }
}

// Usage
const client = new EngagifiiApiClient({
  tenantCode: 'YOUR_TENANT_CODE',
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET'
});

client.getAwards()
  .then(awards => console.log('Awards:', awards))
  .catch(error => console.error('Error:', error.message));

Python Complete Example

python
import requests
import time
import jwt
from typing import Optional, Dict, Any
from datetime import datetime, timedelta

class EngagifiiApiClient:
    def __init__(self, config: Dict[str, str]):
        self.base_url = 'https://engagifii-trainingandaccreditation.azurewebsites.net'
        self.tenant_code = config['tenant_code']
        self.client_id = config.get('client_id')
        self.client_secret = config.get('client_secret')
        self.token = None
        self.token_expires = 0
        
        # Create session with default headers
        self.session = requests.Session()
        self.session.headers.update({
            'tenant-code': self.tenant_code,
            'Content-Type': 'application/json'
        })
    
    def get_token(self) -> Optional[str]:
        """Get valid token, refreshing if necessary"""
        if self.token and time.time() < self.token_expires:
            return self.token
        
        if not self.client_id or not self.client_secret:
            return None
        
        try:
            response = requests.post(
                'https://auth.engagifii.com/token',
                data={
                    'grant_type': 'client_credentials',
                    'client_id': self.client_id,
                    'client_secret': self.client_secret,
                    'scope': 'AccreditationAPI'
                },
                headers={'Content-Type': 'application/x-www-form-urlencoded'}
            )
            response.raise_for_status()
            
            data = response.json()
            self.token = data['access_token']
            self.token_expires = time.time() + data['expires_in'] - 60
            
            return self.token
        except requests.RequestException as e:
            print(f'Failed to get token: {e}')
            return None
    
    def api_call(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make authenticated API call with retry logic"""
        url = f"{self.base_url}{endpoint}"
        
        # Add authentication if available
        token = self.get_token()
        if token:
            self.session.headers['Authorization'] = f'Bearer {token}'
        
        # Retry logic
        max_retries = 3
        for attempt in range(max_retries):
            try:
                response = self.session.request(method, url, **kwargs)
                
                # Handle 401 by refreshing token
                if response.status_code == 401 and attempt < max_retries - 1:
                    self.token = None
                    token = self.get_token()
                    if token:
                        self.session.headers['Authorization'] = f'Bearer {token}'
                        continue
                
                response.raise_for_status()
                return response.json() if response.text else {}
                
            except requests.RequestException as e:
                if attempt == max_retries - 1:
                    raise
                time.sleep(2 ** attempt)  # Exponential backoff
    
    def get_awards(self) -> list:
        """Get list of awards"""
        return self.api_call('GET', '/api/v1/Awards/List')
    
    def create_class_registration(self, registration_data: dict) -> dict:
        """Create a class registration"""
        return self.api_call(
            'POST',
            '/api/v1/registration/ClassRegistration',
            json=registration_data
        )

# Usage
client = EngagifiiApiClient({
    'tenant_code': 'YOUR_TENANT_CODE',
    'client_id': 'YOUR_CLIENT_ID',
    'client_secret': 'YOUR_CLIENT_SECRET'
})

try:
    awards = client.get_awards()
    print(f'Found {len(awards)} awards')
except Exception as e:
    print(f'Error: {e}')

C# Complete Example

csharp
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Collections.Generic;

public class EngagifiiApiClient : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _tenantCode;
    private readonly string _clientId;
    private readonly string _clientSecret;
    private string _accessToken;
    private DateTime _tokenExpires;
    private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
    
    public EngagifiiApiClient(string tenantCode, string clientId = null, string clientSecret = null)
    {
        _tenantCode = tenantCode;
        _clientId = clientId;
        _clientSecret = clientSecret;
        
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://engagifii-trainingandaccreditation.azurewebsites.net"),
            Timeout = TimeSpan.FromSeconds(30)
        };
        
        _httpClient.DefaultRequestHeaders.Add("tenant-code", _tenantCode);
        _httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }
    
    private async Task<string> GetTokenAsync()
    {
        await _tokenSemaphore.WaitAsync();
        try
        {
            if (!string.IsNullOrEmpty(_accessToken) && DateTime.UtcNow < _tokenExpires)
            {
                return _accessToken;
            }
            
            if (string.IsNullOrEmpty(_clientId) || string.IsNullOrEmpty(_clientSecret))
            {
                return null;
            }
            
            var tokenRequest = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("grant_type", "client_credentials"),
                new KeyValuePair<string, string>("client_id", _clientId),
                new KeyValuePair<string, string>("client_secret", _clientSecret),
                new KeyValuePair<string, string>("scope", "AccreditationAPI")
            });
            
            var response = await _httpClient.PostAsync(
                "https://auth.engagifii.com/token", tokenRequest);
            response.EnsureSuccessStatusCode();
            
            var jsonResponse = await response.Content.ReadAsStringAsync();
            dynamic tokenData = JsonConvert.DeserializeObject(jsonResponse);
            
            _accessToken = tokenData.access_token;
            int expiresIn = tokenData.expires_in;
            _tokenExpires = DateTime.UtcNow.AddSeconds(expiresIn - 60);
            
            return _accessToken;
        }
        finally
        {
            _tokenSemaphore.Release();
        }
    }
    
    private async Task<T> ApiCallAsync<T>(HttpMethod method, string endpoint, object data = null)
    {
        // Add authentication if available
        var token = await GetTokenAsync();
        if (!string.IsNullOrEmpty(token))
        {
            _httpClient.DefaultRequestHeaders.Authorization = 
                new AuthenticationHeaderValue("Bearer", token);
        }
        
        HttpResponseMessage response = null;
        
        // Retry logic
        for (int attempt = 0; attempt < 3; attempt++)
        {
            var request = new HttpRequestMessage(method, endpoint);
            
            if (data != null && (method == HttpMethod.Post || method == HttpMethod.Put))
            {
                request.Content = new StringContent(
                    JsonConvert.SerializeObject(data),
                    Encoding.UTF8,
                    "application/json");
            }
            
            response = await _httpClient.SendAsync(request);
            
            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && attempt < 2)
            {
                _accessToken = null;
                token = await GetTokenAsync();
                if (!string.IsNullOrEmpty(token))
                {
                    _httpClient.DefaultRequestHeaders.Authorization = 
                        new AuthenticationHeaderValue("Bearer", token);
                    continue;
                }
            }
            
            break;
        }
        
        response.EnsureSuccessStatusCode();
        
        var jsonResponse = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(jsonResponse);
    }
    
    public async Task<List<Award>> GetAwardsAsync()
    {
        return await ApiCallAsync<List<Award>>(
            HttpMethod.Get, "/api/v1/Awards/List");
    }
    
    public async Task<RegistrationResponse> CreateClassRegistrationAsync(
        ClassRegistration registration)
    {
        return await ApiCallAsync<RegistrationResponse>(
            HttpMethod.Post, 
            "/api/v1/registration/ClassRegistration",
            registration);
    }
    
    public void Dispose()
    {
        _httpClient?.Dispose();
        _tokenSemaphore?.Dispose();
    }
}

// Usage
using (var client = new EngagifiiApiClient(
    "YOUR_TENANT_CODE", 
    "YOUR_CLIENT_ID", 
    "YOUR_CLIENT_SECRET"))
{
    try
    {
        var awards = await client.GetAwardsAsync();
        Console.WriteLine($"Found {awards.Count} awards");
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"API Error: {ex.Message}");
    }
}

Error Scenarios

Authentication Failure Responses

1. Missing Tenant Code

json
{
  "error": "MISSING_TENANT",
  "message": "The tenant-code header is required",
  "statusCode": 400,
  "timestamp": "2024-01-20T10:30:00Z"
}

Resolution: Include tenant-code header in all requests

2. Invalid Tenant Code

json
{
  "error": "INVALID_TENANT",
  "message": "The specified tenant code is not valid",
  "statusCode": 404,
  "timestamp": "2024-01-20T10:30:00Z"
}

Resolution: Verify tenant code with administrator

3. Expired Token

json
{
  "error": "TOKEN_EXPIRED",
  "message": "The access token has expired",
  "statusCode": 401,
  "timestamp": "2024-01-20T10:30:00Z",
  "details": {
    "expired_at": "2024-01-20T09:30:00Z"
  }
}

Resolution: Refresh the access token

4. Invalid Token

json
{
  "error": "INVALID_TOKEN",
  "message": "The access token is invalid",
  "statusCode": 401,
  "timestamp": "2024-01-20T10:30:00Z"
}

Resolution: Obtain a new access token

5. Insufficient Permissions

json
{
  "error": "INSUFFICIENT_PERMISSIONS",
  "message": "You do not have permission to access this resource",
  "statusCode": 403,
  "timestamp": "2024-01-20T10:30:00Z",
  "details": {
    "required_scope": "Awards.Write",
    "your_scopes": ["Awards.Read"]
  }
}

Resolution: Request appropriate permissions from administrator

Handling Authentication Errors

javascript
class AuthErrorHandler {
  static async handle(error, retryCallback) {
    const status = error.response?.status;
    const errorCode = error.response?.data?.error;
    
    switch (status) {
      case 400:
        if (errorCode === 'MISSING_TENANT') {
          throw new Error('Tenant code is required. Please configure your client.');
        }
        break;
        
      case 401:
        if (errorCode === 'TOKEN_EXPIRED') {
          // Attempt to refresh and retry
          await this.refreshToken();
          return retryCallback();
        }
        throw new Error('Authentication failed. Please check your credentials.');
        
      case 403:
        throw new Error('Access denied. You do not have permission for this action.');
        
      case 404:
        if (errorCode === 'INVALID_TENANT') {
          throw new Error('Invalid tenant code. Please verify with your administrator.');
        }
        break;
        
      case 429:
        // Rate limit exceeded
        const retryAfter = error.response?.headers['retry-after'] || 60;
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        return retryCallback();
        
      default:
        throw new Error(`API Error: ${error.message}`);
    }
  }
}

Security Checklist

Before deploying to production, ensure:

  • [ ] All API calls use HTTPS
  • [ ] Tenant code is securely stored and not hardcoded
  • [ ] Tokens are never logged or exposed in URLs
  • [ ] Token refresh logic is implemented
  • [ ] Rate limiting is handled gracefully
  • [ ] Input validation is performed before API calls
  • [ ] Error messages don't expose sensitive information
  • [ ] Secure token storage mechanism is used
  • [ ] SSL certificate validation is enabled
  • [ ] Audit logging is implemented for security events

For additional security concerns or advanced authentication scenarios, contact your system administrator.