Appearance
Authentication Guide
Table of Contents
- Overview
- OAuth 2.0 Implementation
- Getting Access Tokens
- Token Management
- Security Best Practices
- Code Examples
- Error Scenarios
- Troubleshooting
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
- Identity Server: Central authentication server that issues tokens
- Access Token: JWT token used to authenticate API requests
- Client Credentials: Your client ID and secret pair
- 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 responseOAuth 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/tokenRequired Parameters
| Parameter | Description | Example |
|---|---|---|
client_id | Your application's client ID | events_api_client |
client_secret | Your application's client secret | xY9#mK2$pL8@nQ5 |
grant_type | Must be client_credentials | client_credentials |
scope | API scope, must include EventsApi | EventsApi |
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:
| Header | Description | Example |
|---|---|---|
Authorization | Bearer token from Identity Server | Bearer eyJhbGci... |
api-version | API version to use | 1.0 |
tenant-code | Your organization's tenant code | TENANT123 |
Content-Type | Request content type | application/json |
Token Management
Token Lifecycle
- Token Request: Application requests token with credentials
- Token Issuance: Identity Server validates and issues token
- Token Usage: Token included in API requests
- Token Expiration: Default expiry is 3600 seconds (1 hour)
- 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
Token not refreshing:
- Check system time synchronization
- Verify refresh logic triggers before expiry
- Ensure thread-safe token refresh
Intermittent 401 errors:
- Implement retry logic with exponential backoff
- Check for race conditions in token refresh
- Verify token cache implementation
SSL/TLS errors:
- Update to latest TLS libraries
- Verify certificate chain is complete
- Check proxy/firewall settings
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()
}