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.

πŸ“ Note: This documentation uses placeholder URLs. Replace {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).

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:

  1. Application authenticates with user credentials and client credentials
  2. Receives access token
  3. 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:

  1. Generate code verifier and challenge
  2. Redirect user to authorization endpoint
  3. User authenticates and consents
  4. Receive authorization code
  5. 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)

Not Recommended: This flow has been replaced with the password flow for API authentication. Use the password flow instead.

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:

1

Access Identity Server

Navigate to https://{YOUR_IDENTITY_SERVER} and log in with your admin account

2

Navigate to API Clients

Go to Account β†’ Manage β†’ API Clients

3

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
4

Save Credentials

Copy and securely store:

  • Client ID: Public identifier
  • Client Secret: Private key (confidential clients only)
Need Help? See our detailed API Clients Registration Guide for step-by-step instructions with screenshots.

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:

  1. Token Level: Scopes embedded in JWT
  2. Endpoint Level: Required scopes per endpoint
  3. 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
Security Reminder: Always follow the principle of least privilege. Request only the minimum scopes necessary for your application's functionality.

Additional Resources