Appearance
Authentication & Security Guide
Table of Contents
- Overview
- Authentication Methods
- Token Acquisition
- Token Management
- Security Best Practices
- Authentication Examples
- Error Scenarios
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/jsonTenant 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/json3. 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/jsonToken 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 FalseToken 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.
