Authentication¶
ESO Logs Python uses OAuth2 authentication to securely access the ESO Logs API v2.
Prerequisites¶
Before you can authenticate, you need:
- ESO Logs Account: Create a free account at esologs.com
- API Client: Register an application to get your credentials
- Environment Setup: Configure your credentials securely
Creating an API Client¶
Step 1: Register Your Application¶
- Visit ESO Logs API Clients
- Click "+ Create Client" (top right corner)
Fill out the application form:
Field Value Notes Application Name Your Application Name e.g., "My ESO Analysis Tool" - be descriptive Redirect URLs See below Required for OAuth2 user authentication flow Public Client Leave unchecked Only check if you cannot store client secret securely For Redirect URLs:
- Client Credentials Only: Leave blank if you only need API access (no user data)
- User Authentication: Enter URLs for OAuth2 callbacks (comma-separated if multiple)
- Development:
http://localhost:8765/callback(recommended port for this library) - Production:
https://yourdomain.com/auth/callback
- Development:
- Multiple URLs supported, separated by commas
Application Naming
Be descriptive with your application name. As noted in the form: "If we can't understand what the application is, we're more likely to cancel the key."
Public Client vs Private Client
- Private Client (Recommended): Can securely store client secret. Use for server-side applications, CLI tools, and scripts.
- Public Client: Cannot store client secret securely. Uses PKCE (Proof Key for Code Exchange) flow. Mainly for mobile apps or browser-based applications.
For ESO Logs Python library usage, keep "Public Client" unchecked unless you have specific security constraints.
Click "Create"
Step 2: Get Your Credentials¶
After creating your client, you'll be returned to the "Manage Your Clients" page where your new client will be listed. Each client displays:
- Client Name: The name you provided
- Client ID: The public identifier (visible in the listing)
- Homepage URL: If you provided one during creation
- Edit/Delete buttons: For managing your client
To access your credentials:
- Click "Edit" on your client
- You'll see your Client ID and Client Secret
- Copy both values for use in your application
Credentials you'll receive: - Client ID: Public identifier (like a username) - visible in listings - Client Secret: Private key (only visible when editing) - keep this secure!
Keep Your Secret Safe
Never commit your Client Secret to version control or share it publicly. Treat it like a password - store it securely using environment variables.
Setting Up Credentials¶
Method 1: Environment Variables (Recommended)¶
Set your credentials as environment variables:
# Set for current session
$env:ESOLOGS_ID="your_client_id_here"
$env:ESOLOGS_SECRET="your_client_secret_here"
# Set permanently (requires restart)
[Environment]::SetEnvironmentVariable("ESOLOGS_ID", "your_client_id_here", "User")
[Environment]::SetEnvironmentVariable("ESOLOGS_SECRET", "your_client_secret_here", "User")
Method 2: .env File¶
Create a .env file in your project root:
Security Warning
Add .env to your .gitignore file to prevent committing credentials:
Method 3: Direct Parameter Passing¶
For testing or specific use cases, you can pass credentials directly:
from esologs.auth import get_access_token
# Direct parameter passing (not recommended for production)
token = get_access_token(
client_id="your_client_id",
client_secret="your_client_secret"
)
Using Authentication¶
Basic Authentication¶
from esologs.auth import get_access_token
# Get access token using environment variables
token = get_access_token()
print(f"Access token: {token[:20]}...") # Show first 20 chars
With the Client¶
import asyncio
from esologs.client import Client
from esologs.auth import get_access_token
async def main():
# Get authentication token
token = get_access_token()
# Create authenticated client
async with Client(
url="https://www.esologs.com/api/v2/client",
headers={"Authorization": f"Bearer {token}"}
) as client:
# Test authentication with rate limit check
rate_limit = await client.get_rate_limit_data()
print(f"Rate limit: {rate_limit.rate_limit_data.limit_per_hour}")
print(f"Points used: {rate_limit.rate_limit_data.points_spent_this_hour}")
asyncio.run(main())
Error Handling¶
import asyncio
from esologs.auth import get_access_token
from esologs.client import Client
from esologs.exceptions import GraphQLClientHttpError
async def test_authentication():
try:
token = get_access_token()
print("✅ Token obtained successfully")
# Test token with API call
async with Client(
url="https://www.esologs.com/api/v2/client",
headers={"Authorization": f"Bearer {token}"}
) as client:
rate_limit = await client.get_rate_limit_data()
print("✅ Authentication successful")
print(f"Rate limit: {rate_limit.rate_limit_data.limit_per_hour}/hour")
except GraphQLClientHttpError as e:
if e.status_code == 401:
print("❌ Authentication failed: Invalid credentials")
print("Check your ESOLOGS_ID and ESOLOGS_SECRET environment variables")
else:
print(f"❌ HTTP error: {e.status_code}")
except Exception as e:
print(f"❌ Unexpected error: {e}")
asyncio.run(test_authentication())
OAuth2 User Authentication¶
For accessing user-specific data, you need to implement OAuth2 Authorization Code flow.
Quick Start with OAuth2Flow (Recommended)¶
The simplest way to handle OAuth2 user authentication is with the OAuth2Flow class:
from esologs import OAuth2Flow, Client
import asyncio
# Create OAuth2 flow handler
oauth_flow = OAuth2Flow(
client_id="your_client_id",
client_secret="your_client_secret",
redirect_uri="http://localhost:8765/callback" # Must match your ESO Logs app config
)
# This will:
# 1. Start a local server on port 8765
# 2. Open your browser for authorization
# 3. Capture the callback automatically
# 4. Exchange the code for a token
user_token = oauth_flow.authorize(scopes=["view-user-profile"])
# Use the token
async def main():
async with Client(
url="https://www.esologs.com/api/v2/user",
user_token=user_token
) as client:
current_user = await client.get_current_user()
print(f"Logged in as: {current_user.user_data.current_user.name}")
asyncio.run(main())
Redirect URI Configuration
Make sure to add http://localhost:8765/callback to your ESO Logs app's redirect URLs. The OAuth2Flow class extracts the port from your redirect URI automatically.
OAuth2 Error Handling¶
Handle common OAuth2 errors gracefully:
from esologs import OAuth2Flow, Client
from esologs.exceptions import GraphQLClientHttpError
import asyncio
# Create OAuth2 flow handler
oauth_flow = OAuth2Flow(
client_id="your_client_id",
client_secret="your_client_secret",
redirect_uri="http://localhost:8765/callback"
)
try:
# Attempt authorization
user_token = oauth_flow.authorize(scopes=["view-user-profile"])
# Test the token
async def verify_token():
try:
async with Client(
url="https://www.esologs.com/api/v2/user",
user_token=user_token
) as client:
user = await client.get_current_user()
print(f"✅ Successfully logged in as: {user.user_data.current_user.name}")
return True
except GraphQLClientHttpError as e:
if e.status_code == 401:
print("❌ Token is invalid or expired")
else:
print(f"❌ API error: {e}")
return False
asyncio.run(verify_token())
except ValueError as e:
print(f"❌ Configuration error: {e}")
print("Check your redirect URI matches ESO Logs app settings")
except KeyboardInterrupt:
print("\n⚠️ Authorization cancelled by user")
except Exception as e:
print(f"❌ Unexpected error during OAuth2 flow: {e}")
OAuth2 State Validation¶
For enhanced security, validate the state parameter to prevent CSRF attacks:
from esologs.user_auth import generate_authorization_url, exchange_authorization_code
import secrets
import webbrowser
# Generate secure state
state = secrets.token_urlsafe(32)
# Store state in your session (web apps) or memory
stored_state = state
# Generate authorization URL with state
auth_url = generate_authorization_url(
client_id="your_client_id",
redirect_uri="http://localhost:8000/callback",
scopes=["view-user-profile"],
state=state
)
# In your callback handler:
def handle_callback(request):
# Get state from callback
returned_state = request.args.get('state')
code = request.args.get('code')
error = request.args.get('error')
# Validate state
if not returned_state or returned_state != stored_state:
raise ValueError("Invalid state parameter - possible CSRF attack")
# Check for errors
if error:
error_desc = request.args.get('error_description', 'Unknown error')
raise ValueError(f"Authorization failed: {error} - {error_desc}")
# Exchange code for token
try:
user_token = exchange_authorization_code(
client_id="your_client_id",
client_secret="your_client_secret",
code=code,
redirect_uri="http://localhost:8000/callback"
)
return user_token
except Exception as e:
raise ValueError(f"Token exchange failed: {e}")
State Parameter Security
Always use and validate the state parameter in production applications to prevent CSRF attacks. The OAuth2Flow class handles this automatically, but manual implementations must include it.
Async OAuth2 Flow¶
For async applications, use the AsyncOAuth2Flow class:
from esologs import AsyncOAuth2Flow, Client
import asyncio
async def main():
# Create async OAuth2 flow handler
oauth_flow = AsyncOAuth2Flow(
client_id="your_client_id",
client_secret="your_client_secret",
redirect_uri="http://localhost:8765/callback"
)
# Authorize asynchronously
user_token = await oauth_flow.authorize(scopes=["view-user-profile"])
# Use the token
async with Client(
url="https://www.esologs.com/api/v2/user",
user_token=user_token
) as client:
current_user = await client.get_current_user()
print(f"Logged in as: {current_user.user_data.current_user.name}")
asyncio.run(main())
Manual OAuth2 Flow¶
For more control or custom implementations, you can use the manual flow:
Setting Up User Authentication¶
from esologs.user_auth import (
generate_authorization_url,
exchange_authorization_code,
refresh_access_token,
UserToken
)
# Step 1: Generate authorization URL
import secrets
state = secrets.token_urlsafe(32)
auth_url = generate_authorization_url(
client_id="your_client_id",
redirect_uri="http://localhost:8000/callback",
scopes=["view-user-profile"],
state=state # Optional CSRF protection
)
print(f"Please visit: {auth_url}")
# User will be redirected to ESO Logs login page
Handling the Callback¶
After authorization, the user is redirected to your callback URL with a code:
# Step 2: Exchange authorization code for token
# (In your callback handler)
code = request.args.get('code') # Get from callback URL
user_token = exchange_authorization_code(
client_id="your_client_id",
client_secret="your_client_secret",
code=code,
redirect_uri="http://localhost:8000/callback"
)
# user_token contains:
# - access_token: Bearer token for API requests
# - refresh_token: Token to refresh expired access
# - expires_in: Token lifetime in seconds
Async Manual Flow¶
Use async variants for better performance in async applications:
from esologs.user_auth import exchange_authorization_code_async
# Exchange code asynchronously
user_token = await exchange_authorization_code_async(
client_id="your_client_id",
client_secret="your_client_secret",
code=code,
redirect_uri="http://localhost:8000/callback"
)
Using User Authentication¶
import asyncio
from esologs.client import Client
async def get_user_info():
# Note: Using /api/v2/user endpoint (not /client)
async with Client(
url="https://www.esologs.com/api/v2/user",
user_token=user_token # Can be UserToken object or string
) as client:
# Get current user information
current_user = await client.get_current_user()
print(f"Logged in as: {current_user.user_data.current_user.name}")
# Access user's guilds
for guild in current_user.user_data.current_user.guilds:
print(f"Guild: {guild.name} on {guild.server.name}")
asyncio.run(get_user_info())
Token Persistence¶
Save and load tokens for persistent authentication:
from esologs.user_auth import save_token_to_file, load_token_from_file
# Save token after authentication
save_token_to_file(user_token, ".esologs_token.json")
# Load token later
saved_token = load_token_from_file(".esologs_token.json")
if saved_token and saved_token.is_expired:
# Refresh if expired
new_token = refresh_access_token(
client_id="your_client_id",
client_secret="your_client_secret",
refresh_token=saved_token.refresh_token
)
save_token_to_file(new_token)
Async Token Persistence¶
For async applications, use the async variants:
from esologs.user_auth import (
save_token_to_file_async,
load_token_from_file_async,
refresh_access_token_async
)
# Save token asynchronously
await save_token_to_file_async(user_token, ".esologs_token.json")
# Load token asynchronously
saved_token = await load_token_from_file_async(".esologs_token.json")
if saved_token and saved_token.is_expired:
# Refresh asynchronously
new_token = await refresh_access_token_async(
client_id="your_client_id",
client_secret="your_client_secret",
refresh_token=saved_token.refresh_token
)
await save_token_to_file_async(new_token)
Complete Flask Example¶
Here's a complete example web app with OAuth2 flow:
from flask import Flask, request, redirect, session
from esologs.user_auth import (
generate_authorization_url,
exchange_authorization_code,
save_token_to_file
)
from esologs.client import Client
import asyncio
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
REDIRECT_URI = "http://localhost:5000/callback"
@app.route('/')
def home():
return '<a href="/login">Login with ESO Logs</a>'
@app.route('/login')
def login():
# Generate CSRF token
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
# Generate authorization URL
auth_url = generate_authorization_url(
client_id=CLIENT_ID,
redirect_uri=REDIRECT_URI,
scopes=["view-user-profile"],
state=state
)
return redirect(auth_url)
@app.route('/callback')
def callback():
# Verify CSRF token
state = request.args.get('state')
if state != session.get('oauth_state'):
return "Invalid state parameter", 400
# Get authorization code
code = request.args.get('code')
if not code:
return "Authorization failed", 400
try:
# Exchange code for token
user_token = exchange_authorization_code(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
code=code,
redirect_uri=REDIRECT_URI
)
# Save token for later use
save_token_to_file(user_token)
session['user_token'] = user_token.access_token
return redirect('/profile')
except Exception as e:
return f"Error: {str(e)}", 500
@app.route('/profile')
def profile():
token = session.get('user_token')
if not token:
return redirect('/login')
# Get user info asynchronously
user_info = asyncio.run(get_user_profile(token))
return f"""
<h1>Welcome, {user_info['name']}!</h1>
<p>User ID: {user_info['id']}</p>
<p>Guilds: {', '.join(user_info['guilds'])}</p>
<p>Characters: {', '.join(user_info['characters'])}</p>
<a href="/logout">Logout</a>
"""
async def get_user_profile(token):
async with Client(
url="https://www.esologs.com/api/v2/user",
user_token=token
) as client:
current_user = await client.get_current_user()
user = current_user.user_data.current_user
return {
'name': user.name,
'id': user.id,
'guilds': [g.name for g in user.guilds],
'characters': [c.name for c in user.characters]
}
@app.route('/logout')
def logout():
session.clear()
return redirect('/')
if __name__ == '__main__':
app.run(port=5000, debug=True)
Complete FastAPI Async Example¶
Here's a modern async example using FastAPI:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse
from esologs.user_auth import (
generate_authorization_url,
exchange_authorization_code_async,
save_token_to_file_async,
load_token_from_file_async,
refresh_access_token_async
)
from esologs.client import Client
import secrets
app = FastAPI()
CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
REDIRECT_URI = "http://localhost:8000/callback"
# In-memory session store (use Redis in production)
sessions = {}
@app.get("/")
async def home():
return {"message": "Visit /login to authenticate with ESO Logs"}
@app.get("/login")
async def login():
# Generate CSRF token
state = secrets.token_urlsafe(32)
# Generate authorization URL
auth_url = generate_authorization_url(
client_id=CLIENT_ID,
redirect_uri=REDIRECT_URI,
scopes=["view-user-profile"],
state=state
)
# Store state for verification (use Redis/database in production)
sessions[state] = {"status": "pending"}
return RedirectResponse(url=auth_url)
@app.get("/callback")
async def callback(code: str, state: str):
# Verify CSRF token
if state not in sessions:
raise HTTPException(status_code=400, detail="Invalid state parameter")
try:
# Exchange code for token asynchronously
user_token = await exchange_authorization_code_async(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
code=code,
redirect_uri=REDIRECT_URI
)
# Save token asynchronously
await save_token_to_file_async(user_token, ".esologs_token.json")
# Store token in session
sessions[state] = {"status": "authenticated", "token": user_token.access_token}
return RedirectResponse(url="/profile?state=" + state)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/profile")
async def profile(state: str):
if state not in sessions or sessions[state]["status"] != "authenticated":
return RedirectResponse(url="/login")
token = sessions[state]["token"]
# Get user info asynchronously
async with Client(
url="https://www.esologs.com/api/v2/user",
user_token=token
) as client:
current_user = await client.get_current_user()
user = current_user.user_data.current_user
return {
"name": user.name,
"id": user.id,
"guilds": [g.name for g in user.guilds],
"characters": [c.name for c in user.characters]
}
@app.get("/refresh")
async def refresh_token():
# Load saved token
saved_token = await load_token_from_file_async(".esologs_token.json")
if not saved_token:
raise HTTPException(status_code=401, detail="No saved token found")
if saved_token.is_expired:
# Refresh asynchronously
new_token = await refresh_access_token_async(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
refresh_token=saved_token.refresh_token
)
await save_token_to_file_async(new_token, ".esologs_token.json")
return {"message": "Token refreshed successfully"}
return {"message": "Token is still valid"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Important Notes¶
- Different Endpoint: User authentication requires
/api/v2/userendpoint, not/api/v2/client - Scopes: Request
view-user-profilescope to access user's guilds and characters - Security: Always validate the
stateparameter to prevent CSRF attacks - Token Storage: Never expose tokens in URLs or client-side code
- Refresh Tokens: Store refresh tokens securely to maintain persistent authentication
Authentication Flow¶
ESO Logs Python supports two OAuth2 authentication flows:
Client Credentials Flow (Default)¶
Used for most API access:
graph LR
A[Your App] --> B[get_access_token()]
B --> C[ESO Logs OAuth2]
C --> D[Access Token]
D --> E[API Requests]
E --> F[ESO Logs API v2]- Client Registration: Your app is registered with ESO Logs
- Token Request: App requests access token using credentials
- Token Response: ESO Logs returns a bearer token
- API Access: Token is used for authenticated API requests
- Token Refresh: Tokens are automatically refreshed as needed
Authorization Code Flow (User Authentication)¶
Required for user-specific data (UserData endpoints):
graph LR
A[User] --> B[Authorization URL]
B --> C[ESO Logs Login]
C --> D[User Authorizes]
D --> E[Callback with Code]
E --> F[Exchange Code]
F --> G[User Token]
G --> H[User API Access]- Generate Auth URL: Create URL for user to authorize
- User Authorization: User logs in and grants permissions
- Authorization Code: User redirected back with code
- Token Exchange: Exchange code for access/refresh tokens
- API Access: Use token for user-specific endpoints
Token Management¶
Automatic Token Refresh¶
ESO Logs Python automatically handles token refresh:
- Tokens are cached and reused until expiration
- New tokens are requested automatically when needed
- No manual token management required
Token Validation¶
Verify your token is working:
import asyncio
from esologs.client import Client
from esologs.auth import get_access_token
async def validate_token():
"""Validate authentication token by making a simple API call."""
try:
token = get_access_token()
async with Client(
url="https://www.esologs.com/api/v2/client",
headers={"Authorization": f"Bearer {token}"}
) as client:
# Simple validation call
rate_limit = await client.get_rate_limit_data()
print("✅ Token valid")
print(f"Rate limit: {rate_limit.rate_limit_data.limit_per_hour}/hour")
print(f"Used: {rate_limit.rate_limit_data.points_spent_this_hour}")
return True
except Exception as e:
print(f"❌ Token validation failed: {e}")
return False
# Run validation
asyncio.run(validate_token())
Security Best Practices¶
Environment Variables¶
✅ Do: - Use environment variables for production - Add to your shell profile for persistence - Use different credentials for development/production
❌ Don't: - Hard-code credentials in source code - Commit credentials to version control - Share credentials in chat/email
File-based Configuration¶
If using .env files:
# config.py
import os
from pathlib import Path
# Load from .env file
def load_env():
env_path = Path('.env')
if env_path.exists():
with open(env_path) as f:
for line in f:
if line.strip() and not line.startswith('#'):
key, value = line.strip().split('=', 1)
os.environ[key] = value
load_env()
Production Deployment¶
For production environments:
- Use secure environment variable management
- Consider services like AWS Secrets Manager, Azure Key Vault
- Implement credential rotation
- Monitor API usage and rate limits
Troubleshooting¶
Common Authentication Errors¶
OAuth2 Redirect URI Mismatch¶
Solutions: 1. Ensure your redirect URI exactly matches one configured in your ESO Logs app 2. Check for trailing slashes - http://localhost:8765/callback vs http://localhost:8765/callback/ 3. Verify the port number matches 4. Add multiple redirect URIs for different environments (dev/prod)
Invalid Client Credentials¶
Exception: OAuth request failed with status 401: {"error":"invalid_client","error_description":"Client authentication failed","message":"Client authentication failed"}
Solutions: 1. Verify your Client ID and Secret are correct 2. Check for extra spaces or hidden characters 3. Ensure environment variables are set properly 4. Try regenerating your Client Secret on the ESO Logs website
Rate Limit Exceeded¶
Solutions: 1. Check your current usage with get_rate_limit_data() 2. Implement request throttling in your application 3. Consider upgrading your ESO Logs plan 4. Cache responses to reduce API calls
Network Connection Issues¶
orSolutions: 1. Check your internet connection 2. Verify ESO Logs API status 3. Check firewall/proxy settings 4. Try again after a brief delay
Debugging Authentication¶
Enable debug logging to troubleshoot issues:
import logging
from esologs.auth import get_access_token
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# Get token with debug info
token = get_access_token()
Next Steps¶
With authentication configured:
- Start with Quickstart - Make your first API calls
- Read API Reference - Understand available methods with examples
- Development Guide - Set up for contributing
Rate Limits
ESO Logs API has rate limits based on points per hour. Use get_rate_limit_data() to monitor your usage and avoid hitting limits.
Multiple Applications
You can create multiple API clients for different applications or environments. Each client gets its own rate limit allocation.