Skip to main content

Authentication

The Tracker GraphQL API uses JWT (JSON Web Tokens) for authentication. Each request must include a valid JWT token with specific claims.

info

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 ID
  • client_list: Array of client IDs that the user has access to
  • exp: 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:

  1. Call the refresh token endpoint:
mutation RefreshToken {
refreshToken {
token
expiresIn
}
}
  1. Replace your old token with the new one

Error Handling

Common authentication errors:

{
"errors": [
{
"message": "Invalid token",
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
Error CodeDescription
UNAUTHENTICATEDMissing or invalid token
UNAUTHORIZEDToken valid but insufficient permissions
TOKEN_EXPIREDToken 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

  1. Token Security

    • Store tokens securely
    • Never share tokens between applications
    • Implement token refresh before expiration
    • Use HTTPS for all API requests
    • Rotate tokens regularly
  2. 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
  3. 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
  4. 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