Authentication
The Tracker GraphQL API uses JWT (JSON Web Tokens) for authentication. Each request must include a valid JWT token with specific claims.
For a detailed explanation of the authentication flow and login process, see the Authentication Flow documentation.
JWT Token Structure
The JWT token must include the following claims:
{
"sub": "user_id",
"client_list": [1, 2], // Array of integer client IDs
"exp": 1735689600 // Expiration timestamp
}
Required Claims
sub: The user IDclient_list: Array of client IDs that the user has access toexp: Token expiration timestamp
Optional Claims
roles: Array of role strings that define special permissions (e.g.,["admin"])
Using the Token
Include the JWT token in the HTTP Authorization header:
Authorization: Bearer your.jwt.token
For GraphQL requests:
const client = new GraphQLClient("https://your-api/graphql", {
headers: {
Authorization: `Bearer ${token}`,
},
});
Client List Filtering
The client_list claim is used to filter data access. When the client_list is empty:
- Collection queries (e.g.,
brands,clients) return a 404 error - Single item queries (e.g.,
brand,client) return null - Mutations will fail with appropriate error messages
When client_list contains valid client IDs:
- Queries will only return data for clients in the list
- Mutations will only affect resources owned by these clients
- Real-time updates will only include events for these clients
Example error for empty client_list:
{
"errors": [
{
"message": "No authorized clients found",
"extensions": {
"code": "NOT_FOUND",
"http_status": 404
}
}
]
}
Example query with client filtering:
query GetTrackers {
trackers {
# Only returns trackers for clients in client_list (e.g., [1, 2])
id
status
client {
id # Integer client ID
name
}
}
}
Note: The client_list claim contains integer client IDs, not strings. Make sure to handle type conversion appropriately in your code.
Admin Role
Users with the admin role in their JWT token can bypass client list filtering, allowing them to access data for all clients in the system. This is implemented through the roles claim in the JWT token.
{
"sub": "user_id",
"client_list": [1, 2],
"roles": ["admin"],
"exp": 1735689600
}
When a user has the admin role:
- They can view data for all clients, regardless of their client_list
- Client filtering is bypassed in all queries
- They have full visibility across the entire system
For more details on the admin role, see the Admin Role documentation.
Token Renewal
Tokens expire after a set period. To get a new token:
- Call the refresh token endpoint:
mutation RefreshToken {
refreshToken {
token
expiresIn
}
}
- Replace your old token with the new one
Error Handling
Common authentication errors:
{
"errors": [
{
"message": "Invalid token",
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
| Error Code | Description |
|---|---|
UNAUTHENTICATED | Missing or invalid token |
UNAUTHORIZED | Token valid but insufficient permissions |
TOKEN_EXPIRED | Token has expired |
Authentication Patterns in Resolvers
Resolvers in the application use the get_client_list_from_token helper function to enforce data access based on the client_list claim in the JWT token.
Basic Pattern
@query.field("clients")
async def resolve_clients(_, info):
db = SessionLocal()
try:
# Get client list from token (returns list of integers)
client_list = await get_client_list_from_token(info)
is_admin = await is_admin_from_token(info)
# Skip filtering for admin users
if is_admin:
return db.query(Client).all()
# Filter query based on client_list integers
clients = (
db.query(Client)
.filter(Client.id.in_(client_list))
.all()
)
return clients
finally:
db.close()
Relationship Filtering
For models with client relationships, use SQL-level filtering and handle empty client_list:
@query.field("brands")
async def resolve_brands(_, info):
db = SessionLocal()
try:
client_list = await get_client_list_from_token(info)
is_admin = await is_admin_from_token(info)
# Skip client filtering for admin users
if is_admin:
return db.query(Brand).all()
# Return 404 if client_list is empty
if not client_list:
raise GraphQLError(
"No authorized clients found",
extensions={"code": "NOT_FOUND", "http_status": 404},
)
# Filter on client relationship
brands = (
db.query(Brand)
.filter(Brand.client_id.in_(client_list))
.all()
)
return brands
finally:
db.close()
Single Item Access
For single item queries, verify the item belongs to an allowed client:
@query.field("client")
async def resolve_client(_, info, id):
client_list = await get_client_list_from_token(info)
# Return None if client not in allowed list
# Convert string ID to integer for comparison with client_list integers
try:
client_id = int(id)
if client_id not in client_list:
return None
except (ValueError, TypeError):
return None
# Proceed with query if authorized
return db.query(Client).filter(Client.id == id).first()
Authentication in Mutations
Mutations must also verify client access before allowing modifications.
Create Operations
For create operations, verify the client has permission:
@mutation.field("createBrand")
async def resolve_create_brand(_, info, input):
client_list = await get_client_list_from_token(info)
# Verify client access
# Convert clientId to integer for comparison
try:
client_id = int(input.get("clientId"))
if client_id not in client_list:
raise GraphQLError("Unauthorized: Cannot create brand for this client")
except (ValueError, TypeError):
raise GraphQLError("Invalid client ID format")
# Proceed with creation if authorized
brand = Brand(**input)
db.add(brand)
db.commit()
return brand
Update Operations
For updates, verify ownership before allowing changes:
@mutation.field("updateTracker")
async def resolve_update_tracker(_, info, id, input):
client_list = await get_client_list_from_token(info)
# Verify tracker belongs to allowed client
tracker = db.query(Tracker).filter(
and_(
Tracker.id == id,
Tracker.client_id.in_(client_list)
)
).first()
if not tracker:
raise GraphQLError("Tracker not found or unauthorized")
# Proceed with update if authorized
for key, value in input.items():
setattr(tracker, key, value)
db.commit()
return tracker
Best Practices
-
Token Security
- Store tokens securely
- Never share tokens between applications
- Implement token refresh before expiration
- Use HTTPS for all API requests
- Rotate tokens regularly
-
Client List Handling
- Always fetch client_list at the start of resolvers
- Use SQL-level filtering with client_list for efficiency
- Handle both direct client relationships and nested relationships
- Return empty lists/None instead of errors for unauthorized access
- Include client_list checks in both queries and mutations
-
Error Handling
- Return None for single items when unauthorized
- Return empty lists for collection queries when unauthorized
- Raise explicit GraphQLError for unauthorized mutations
- Log unauthorized access attempts for security monitoring
-
Performance
- Use SQL-level filtering instead of in-memory filtering
- Include client_list conditions in the initial query
- Use appropriate indexes on client_id columns
- Optimize queries with joins and eager loading when needed