Authentication & Authorization
Secure your API integration with OAuth 2.0 and OpenID Connect. This comprehensive guide covers authentication flows, token management, scopes, and security best practices.
{YOUR_IDENTITY_SERVER} with your Identity Server URL (e.g., identity.yourdomain.com or localhost:44367 for local development) and {YOUR_API_SERVER} with your API Server URL (e.g., api.yourdomain.com or localhost:44395 for local development).
On this page
Overview
Exepron uses industry-standard OAuth 2.0 and OpenID Connect protocols for API authentication and authorization. This ensures secure access to resources while maintaining flexibility for different application types.
Key Components
| Component | Role | Description |
|---|---|---|
| Resource Owner | End User | The Exepron user who owns the data and grants access permissions |
| Client | Your Application | The application requesting access to Exepron resources |
| Authorization Server | Exepron Identity Server | Issues tokens after authenticating users (https://{YOUR_IDENTITY_SERVER}) |
| Resource Server | Exepron REST API | Hosts protected resources (https://{YOUR_API_SERVER}) |
Authentication Flow Diagram
ββββββββββββββββ ββββββββββββββββ
β β β β
β Client β β Resource β
β Application β β Owner β
β β β (User) β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β
β 1. Authorization Request β
βββββββββββββββββββββββββββββββββββββββββββββββββββββΆ
β β
β β 2. User Login
β β & Consent
β ββββββββββββββββ β
β β ββββββββββββββββββββ
β β Authorizationβ
β β Server β 3. Authorization Code
β β (Identity) ββββββββββββββββββββ
β β β β
β ββββββββ¬ββββββββ β
β β βΌ
ββββββββββββββββββββββββββΌβββββββββββββββββββββββββββ
β 4. Authorization Code β
β β
β 5. Exchange Code β
ββββββββββββββββββββββββββΆ
β for Tokens β
β β
ββββββββββββββββββββββββββ
β 6. Access Token & β
β Refresh Token β
β β
β β ββββββββββββββββ
β 7. API Request β β β
βββββββββββββββββββββββββββββββββββΆ Resource β
β (with Access Token) β β Server β
β β β (API) β
βββββββββββββββββββββββββββββββββββ β
β 8. Protected Resource β ββββββββββββββββ
β β
βΌ βΌ
OAuth 2.0 Flows
Choose the appropriate OAuth flow based on your application type:
1. Password Flow (Resource Owner Password Credentials)
Use Case: Server-side applications, scripts, or services with user authentication
Security: High - Client secret never exposed to browser
Flow Steps:
- Application authenticates with user credentials and client credentials
- Receives access token
- Uses token to access API
Example Request:
POST /connect/token HTTP/1.1
Host: localhost:44367
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=USER_EMAIL
&password=USER_PASSWORD
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=api1
Example Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5...",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "api1"
}
2. Authorization Code Flow with PKCE (Recommended for SPAs)
Use Case: Single-page applications, mobile apps, native applications
Security: High - Uses PKCE to prevent authorization code interception
Flow Steps:
- Generate code verifier and challenge
- Redirect user to authorization endpoint
- User authenticates and consents
- Receive authorization code
- Exchange code for tokens with code verifier
Step 1: Generate PKCE Parameters
// Generate code verifier (43-128 characters)
const codeVerifier = generateRandomString(128);
// Generate code challenge
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = base64URLEncode(digest);
Step 2: Authorization Request
GET /connect/authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid profile api1
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
Step 3: Token Exchange
POST /connect/token HTTP/1.1
Host: localhost:44367
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=CODE_VERIFIER
3. Client Credentials Flow (Legacy - Machine-to-Machine)
Example Request:
POST /connect/token HTTP/1.1
Host: localhost:44367
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=api1
4. Refresh Token Flow
Use Case: Obtaining new access tokens without user interaction
Example Request:
POST /connect/token HTTP/1.1
Host: localhost:44367
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=YOUR_REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
Example Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5...",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5...",
"scope": "api1 offline_access"
}
Client Registration
Before authenticating, you must register your application in the Exepron Identity Server:
Access Identity Server
Navigate to https://{YOUR_IDENTITY_SERVER} and log in with your admin account
Navigate to API Clients
Go to Account β Manage β API Clients
Create New Client
Click "Create New Client" and configure:
- Client Name: Your application name
- Client Type:
- Public - For SPAs and mobile apps
- Confidential - For server-side applications
- Grant Types: Select appropriate flows
- Redirect URIs: Your callback URLs
- Scopes: Required permissions
Save Credentials
Copy and securely store:
- Client ID: Public identifier
- Client Secret: Private key (confidential clients only)
Token Types
Exepron uses three types of tokens in the authentication process:
1. Access Token
Purpose: Authorize API requests
Format: JWT (JSON Web Token)
Lifetime: 1 hour (3600 seconds)
Usage: Include in Authorization header
Token Structure:
{
"header": {
"alg": "RS256",
"kid": "19A3B5C7D8E9F0A1B2C3D4E5",
"typ": "JWT"
},
"payload": {
"nbf": 1706698800,
"exp": 1706702400,
"iss": "https://{YOUR_IDENTITY_SERVER}",
"aud": "https://{YOUR_API_SERVER}/resources",
"client_id": "your_client_id",
"scope": ["api1"],
"sub": "12345",
"name": "John Doe",
"email": "john.doe@example.com"
},
"signature": "..."
}
Using Access Tokens:
GET /api/v1/projects HTTP/1.1
Host: localhost:44395
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI...
2. Refresh Token
Purpose: Obtain new access tokens
Format: Opaque string or JWT
Lifetime: 30 days (configurable)
Usage: Exchange for new tokens
Best Practices:
- Store securely (encrypted storage)
- Rotate regularly
- Revoke on logout
- One-time use (rotation on use)
3. ID Token
Purpose: User identity claims
Format: JWT
Lifetime: 1 hour
Usage: User profile information
Standard Claims:
{
"sub": "12345", // User ID
"name": "John Doe", // Display name
"email": "john@example.com",
"email_verified": true,
"preferred_username": "johndoe",
"given_name": "John",
"family_name": "Doe",
"iat": 1706698800, // Issued at
"exp": 1706702400 // Expiration
}
Scopes & Permissions
Scopes define the level of access granted to your application:
| Scope | Description | Access Level |
|---|---|---|
api1 |
Full API access | Read/Write all resources |
api1.read |
Read-only API access | Read all resources |
projects |
Project management | CRUD operations on projects |
projects.read |
View projects | Read-only project access |
tasks |
Task management | CRUD operations on tasks |
resources |
Resource management | Manage team and equipment |
reports |
Reporting access | Generate and view reports |
webhooks |
Webhook management | Configure webhooks |
openid |
OpenID Connect | User identity claims |
profile |
User profile | Name, email, etc. |
offline_access |
Refresh tokens | Long-term access |
Requesting Multiple Scopes
// Space-separated list
scope=api1 openid profile offline_access
// URL encoded for HTTP requests
scope=api1%20openid%20profile%20offline_access
Scope Validation
The API validates scopes at multiple levels:
- Token Level: Scopes embedded in JWT
- Endpoint Level: Required scopes per endpoint
- Resource Level: Fine-grained permissions
Implementation Examples
C# / ASP.NET Core
using System;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.Extensions.Configuration;
public class ExepronAuthService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
private TokenResponse _tokenResponse;
private DateTime _tokenExpiry;
public ExepronAuthService(HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
_configuration = configuration;
}
public async Task GetAccessTokenAsync()
{
// Check if token exists and is still valid
if (_tokenResponse != null && DateTime.UtcNow < _tokenExpiry.AddMinutes(-5))
{
return _tokenResponse.AccessToken;
}
// Get new token
var disco = await _httpClient.GetDiscoveryDocumentAsync(
_configuration["Exepron:AuthorityUrl"]);
if (disco.IsError)
{
throw new Exception($"Discovery error: {disco.Error}");
}
// Request token using password flow
_tokenResponse = await _httpClient.RequestPasswordTokenAsync(
new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = _configuration["Exepron:ClientId"],
ClientSecret = _configuration["Exepron:ClientSecret"],
UserName = _configuration["Exepron:UserName"],
Password = _configuration["Exepron:Password"],
Scope = "api1"
});
if (_tokenResponse.IsError)
{
throw new Exception($"Token error: {_tokenResponse.Error}");
}
_tokenExpiry = DateTime.UtcNow.AddSeconds(_tokenResponse.ExpiresIn);
return _tokenResponse.AccessToken;
}
public async Task RefreshTokenAsync(string refreshToken)
{
var disco = await _httpClient.GetDiscoveryDocumentAsync(
_configuration["Exepron:AuthorityUrl"]);
var refreshResponse = await _httpClient.RequestRefreshTokenAsync(
new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = _configuration["Exepron:ClientId"],
ClientSecret = _configuration["Exepron:ClientSecret"],
RefreshToken = refreshToken
});
if (refreshResponse.IsError)
{
throw new Exception($"Refresh error: {refreshResponse.Error}");
}
_tokenResponse = refreshResponse;
_tokenExpiry = DateTime.UtcNow.AddSeconds(refreshResponse.ExpiresIn);
return refreshResponse.AccessToken;
}
}
JavaScript / TypeScript (with PKCE)
class ExepronAuth {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.authorityUrl = config.authorityUrl || 'https://{YOUR_IDENTITY_SERVER}';
this.apiUrl = config.apiUrl || 'https://{YOUR_API_SERVER}';
}
// Generate PKCE challenge
async generatePKCEChallenge() {
const codeVerifier = this.generateRandomString(128);
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = this.base64URLEncode(digest);
// Store verifier for token exchange
sessionStorage.setItem('code_verifier', codeVerifier);
return { codeVerifier, codeChallenge };
}
// Start authorization flow
async authorize() {
const { codeChallenge } = await this.generatePKCEChallenge();
const state = this.generateRandomString(32);
sessionStorage.setItem('auth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: 'openid profile api1 offline_access',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${this.authorityUrl}/connect/authorize?${params}`;
}
// Handle callback and exchange code for tokens
async handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Validate state
const savedState = sessionStorage.getItem('auth_state');
if (state !== savedState) {
throw new Error('Invalid state parameter');
}
// Exchange code for tokens
const codeVerifier = sessionStorage.getItem('code_verifier');
const response = await fetch(`${this.authorityUrl}/connect/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
code_verifier: codeVerifier
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
this.storeTokens(tokens);
// Clean up session storage
sessionStorage.removeItem('code_verifier');
sessionStorage.removeItem('auth_state');
return tokens;
}
// Store tokens securely
storeTokens(tokens) {
// Store in memory for SPA
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token;
this.idToken = tokens.id_token;
// Set expiry time
this.tokenExpiry = Date.now() + (tokens.expires_in * 1000);
// Parse ID token for user info
if (tokens.id_token) {
const payload = this.parseJWT(tokens.id_token);
this.userInfo = payload;
}
}
// Get valid access token (refresh if needed)
async getAccessToken() {
// Check if token exists and is valid
if (this.accessToken && Date.now() < this.tokenExpiry - 60000) {
return this.accessToken;
}
// Refresh token
if (this.refreshToken) {
return await this.refreshAccessToken();
}
// No valid token, need to re-authenticate
throw new Error('No valid token available');
}
// Refresh access token
async refreshAccessToken() {
const response = await fetch(`${this.authorityUrl}/connect/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: this.clientId
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
this.storeTokens(tokens);
return tokens.access_token;
}
// Make authenticated API request
async apiRequest(endpoint, options = {}) {
const token = await this.getAccessToken();
const response = await fetch(`${this.apiUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.status === 401) {
// Try refreshing token once
const newToken = await this.refreshAccessToken();
return fetch(`${this.apiUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
'Content-Type': 'application/json'
}
});
}
return response;
}
// Utility functions
generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let random = '';
const randomValues = crypto.getRandomValues(new Uint8Array(length));
for (let i = 0; i < length; i++) {
random += charset[randomValues[i] % charset.length];
}
return random;
}
base64URLEncode(buffer) {
const bytes = new Uint8Array(buffer);
let string = '';
bytes.forEach(byte => string += String.fromCharCode(byte));
return btoa(string)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
parseJWT(token) {
const parts = token.split('.');
const payload = parts[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
}
// Logout
logout() {
this.accessToken = null;
this.refreshToken = null;
this.idToken = null;
this.userInfo = null;
this.tokenExpiry = null;
// Redirect to logout endpoint
const params = new URLSearchParams({
post_logout_redirect_uri: window.location.origin,
id_token_hint: this.idToken
});
window.location.href = `${this.authorityUrl}/connect/endsession?${params}`;
}
}
// Usage example
const auth = new ExepronAuth({
clientId: 'your-spa-client',
redirectUri: 'https://yourapp.com/callback',
authorityUrl: 'https://{YOUR_IDENTITY_SERVER}',
apiUrl: 'https://{YOUR_API_SERVER}/api/v1'
});
// Start login
auth.authorize();
// Handle callback (on callback page)
auth.handleCallback().then(tokens => {
console.log('Authenticated!', tokens);
// Make API call
auth.apiRequest('/projects').then(response => {
response.json().then(data => console.log(data));
});
});
Python
import requests
import time
import jwt
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
class ExepronAuth:
def __init__(self, client_id: str, client_secret: str,
authority_url: str = "https://{YOUR_IDENTITY_SERVER}",
api_url: str = "https://{YOUR_API_SERVER}"):
self.client_id = client_id
self.client_secret = client_secret
self.authority_url = authority_url
self.api_url = api_url
self.access_token: Optional[str] = None
self.refresh_token: Optional[str] = None
self.token_expiry: Optional[datetime] = None
# For development with self-signed certificates
self.session = requests.Session()
self.session.verify = False # Remove in production!
def authenticate(self, username: str, password: str) -> str:
"""Get access token using password flow"""
token_endpoint = f"{self.authority_url}/connect/token"
data = {
'grant_type': 'password',
'username': username,
'password': password,
'client_id': self.client_id,
'client_secret': self.client_secret,
'scope': 'api1'
}
response = self.session.post(token_endpoint, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data['access_token']
self.token_expiry = datetime.now() + timedelta(
seconds=token_data['expires_in'] - 60
)
return self.access_token
def get_access_token(self, username: str = None, password: str = None) -> str:
"""Get valid access token, refreshing if necessary"""
if self.access_token and self.token_expiry:
if datetime.now() < self.token_expiry:
return self.access_token
# Get new token (requires username/password for initial auth)
if not username or not password:
raise ValueError("Username and password required for initial authentication")
return self.authenticate(username, password)
def refresh_access_token(self) -> str:
"""Refresh the access token using refresh token"""
if not self.refresh_token:
return self.authenticate()
token_endpoint = f"{self.authority_url}/connect/token"
data = {
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret
}
response = self.session.post(token_endpoint, data=data)
if response.status_code != 200:
# Refresh failed, need to re-authenticate with username/password
raise ValueError("Refresh failed. Re-authentication required with username and password.")
token_data = response.json()
self.access_token = token_data['access_token']
self.refresh_token = token_data.get('refresh_token', self.refresh_token)
self.token_expiry = datetime.now() + timedelta(
seconds=token_data['expires_in'] - 60
)
return self.access_token
def api_request(self, endpoint: str, method: str = 'GET',
**kwargs) -> requests.Response:
"""Make an authenticated API request"""
token = self.get_access_token()
headers = kwargs.pop('headers', {})
headers['Authorization'] = f'Bearer {token}'
url = f"{self.api_url}{endpoint}"
response = self.session.request(
method, url, headers=headers, **kwargs
)
# Retry once if unauthorized
if response.status_code == 401:
token = self.refresh_access_token()
headers['Authorization'] = f'Bearer {token}'
response = self.session.request(
method, url, headers=headers, **kwargs
)
return response
def decode_token(self, token: str = None) -> Dict[str, Any]:
"""Decode JWT token to inspect claims"""
token = token or self.access_token
if not token:
raise ValueError("No token available")
# Decode without verification (for debugging only!)
return jwt.decode(token, options={"verify_signature": False})
# Usage example
if __name__ == "__main__":
# Initialize client
auth = ExepronAuth(
client_id="your_client_id",
client_secret="your_client_secret"
)
# Authenticate first
auth.authenticate("user@example.com", "password123")
# Get projects
response = auth.api_request("/api/v1/projects")
if response.ok:
projects = response.json()
print(f"Found {len(projects.get('value', []))} projects")
# Create a new project
new_project = {
"name": "New Project",
"description": "Created via API",
"startDate": "2025-02-01"
}
response = auth.api_request(
"/api/v1/projects",
method="POST",
json=new_project
)
if response.ok:
created = response.json()
print(f"Created project: {created['id']}")
# Inspect token claims
claims = auth.decode_token()
print(f"Token claims: {claims}")
Token Management Best Practices
Token Storage
| Application Type | Recommended Storage | Security Considerations |
|---|---|---|
| Server-side Web App | Server session or encrypted database | Encrypt at rest, secure session management |
| Single Page App (SPA) | Memory only (JavaScript variables) | Never use localStorage, implement silent refresh |
| Mobile App | Secure device storage (Keychain/Keystore) | Use platform-specific secure storage APIs |
| Desktop App | OS credential manager | Encrypt with user-specific keys |
| CLI Tool | Encrypted file in user directory | Set appropriate file permissions |
Token Refresh Strategy
class TokenManager {
constructor(authService) {
this.authService = authService;
this.refreshThreshold = 5 * 60 * 1000; // 5 minutes
}
async getValidToken() {
const token = this.getCurrentToken();
if (!token) {
return await this.authService.authenticate();
}
const expiresIn = this.getTokenExpiry(token) - Date.now();
// Refresh if expires soon
if (expiresIn < this.refreshThreshold) {
try {
return await this.authService.refreshToken();
} catch (error) {
// Fallback to re-authentication
return await this.authService.authenticate();
}
}
return token;
}
getTokenExpiry(token) {
const payload = this.parseJWT(token);
return payload.exp * 1000; // Convert to milliseconds
}
}
Token Rotation
Implement refresh token rotation for enhanced security:
- Issue new refresh token with each use
- Invalidate previous refresh token
- Detect and prevent token replay attacks
- Implement sliding expiration windows
Common Authentication Errors
invalid_client
HTTP Status: 401 Unauthorized
Cause: Invalid client credentials
Solution:
- Verify Client ID and Client Secret
- Check if client is enabled in Identity Server
- Ensure credentials match the registered client
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
invalid_grant
HTTP Status: 400 Bad Request
Cause: Invalid authorization code or refresh token
Solution:
- Authorization code already used or expired
- Refresh token expired or revoked
- PKCE code verifier doesn't match
invalid_scope
HTTP Status: 400 Bad Request
Cause: Requested scope not allowed
Solution:
- Check client's allowed scopes
- Verify scope names are correct
- Request only necessary scopes
unauthorized_client
HTTP Status: 400 Bad Request
Cause: Client not authorized for grant type
Solution:
- Enable required grant type in client configuration
- Use appropriate flow for client type
Token Expired (API)
HTTP Status: 401 Unauthorized
Response Header: WWW-Authenticate: Bearer error="invalid_token", error_description="The token is expired"
Solution:
- Refresh the access token
- Implement automatic token refresh
- Re-authenticate if refresh fails
Security Best Practices
1. Secure Token Storage
- Never store tokens in localStorage or sessionStorage for SPAs
- Use httpOnly, secure cookies for server-side apps
- Encrypt tokens at rest in databases
- Use platform-specific secure storage on mobile
2. Use HTTPS Everywhere
- All authentication endpoints must use HTTPS
- Validate SSL certificates in production
- Implement HSTS headers
- Use TLS 1.2 or higher
3. Implement PKCE
- Required for public clients (SPAs, mobile)
- Use S256 challenge method
- Generate cryptographically random verifiers
- Never reuse code verifiers
4. Token Lifecycle Management
- Use short-lived access tokens (1 hour)
- Implement token refresh before expiry
- Revoke tokens on logout
- Rotate refresh tokens on use
5. Validate Tokens
- Verify token signature
- Check issuer and audience claims
- Validate expiration time
- Verify scope permissions
6. Protect Client Secrets
- Never expose in client-side code
- Use environment variables
- Rotate secrets regularly
- Use key vaults in production
7. Implement Rate Limiting
- Limit token request frequency
- Implement exponential backoff
- Monitor for suspicious patterns
- Block after repeated failures
8. Audit and Monitoring
- Log authentication attempts
- Monitor token usage patterns
- Alert on anomalous behavior
- Regular security audits