Authentication Setup
Hugr supports multiple authentication methods to secure your GraphQL API. Authentication determines user identity and assigns roles that control access permissions.
Overview
Hugr supports the following authentication methods:
- API Keys - Static keys for service-to-service communication
- Anonymous - Unauthenticated access with limited permissions
- OAuth2/JWT - Token-based authentication with standard JWT
- OIDC - OpenID Connect for enterprise identity providers
Each authentication method assigns roles to users, which are then used by the Access Control system to determine permissions.
Configuration
Authentication is configured in the Hugr configuration file. The configuration defines which authentication methods are enabled and how roles are assigned.
Configuration File Location
The authentication configuration is typically located in:
config.yaml- Main configuration fileconfig.json- Alternative JSON format
See the hugr repository for detailed configuration examples.
API Key Authentication
API keys are ideal for service-to-service communication and automated access. Hugr supports two types of API keys:
- Static API Keys - Defined in configuration files
- Managed API Keys - Stored in database and managed dynamically through GraphQL
Static API Keys
Static API keys are defined directly in the configuration file.
Configuration
auth:
api_keys:
enabled: true
header_username: "X-API-Username"
header_user_id: "X-API-User-ID"
keys:
- key: "service_key_abc123"
role: "service"
username: "api_service"
user_id: "svc_001"
- key: "admin_key_xyz789"
role: "admin"
username: "admin_service"
user_id: "admin_001"
Usage
Include the API key and user information in request headers:
curl -X POST https://api.example.com/graphql \
-H "Authorization: Bearer service_key_abc123" \
-H "X-API-Username: api_service" \
-H "X-API-User-ID: svc_001" \
-H "Content-Type: application/json" \
-d '{"query": "{ customers { id name } }"}'
Managed API Keys
Managed API keys allow dynamic management of API keys through the database without restarting the service. Keys are stored in the api_keys table in the core database module and can be created, updated, or deleted via GraphQL.
Enabling Managed API Keys
Enable managed API keys in the configuration:
auth:
api_keys:
enabled: true
managed: true # Enable managed API keys from database
header_username: "X-API-Username" # Optional: for custom user info
header_user_id: "X-API-User-ID" # Optional: for custom user info
When managed: true, Hugr will:
- Check static keys from configuration first
- Then check managed keys from the
api_keystable - Use the first matching valid key
API Keys Table Schema
The api_keys table has the following structure:
type api_keys @module(name: "core")
@table(name: "api_keys")
@unique(fields: ["key"], query_suffix: "by_key") {
"""
API key name (primary key)
"""
name: String! @pk
"""
API key description
"""
description: String!
"""
API key value (unique)
"""
key: String!
"""
Default role for the API key
"""
default_role: String @field_references(
references_name: "roles",
field: "name",
references_query: "api_keys"
)
"""
API key disabled flag
"""
disabled: Boolean @default(value: false)
"""
API key temporal flag (expires_at is required)
"""
is_temporal: Boolean @default(value: false)
"""
API key expiration date
"""
expires_at: Timestamp
"""
Headers for getting user info from request with the API key
JSON: {"role": "x-role", "user_id": "x-user-id", "user_name": "x-user-name"}
"""
headers: JSON
"""
Claims for the API key
JSON: {"role": "role", "user_id": "user-id", "user_name": "user-name"}
"""
claims: JSON
}
Creating Managed API Keys
Create a basic API key:
mutation {
core {
insert_api_keys(data: {
name: "mobile_app_key"
description: "API key for mobile application"
key: "mobile_app_abc123xyz456"
default_role: "app_user"
}) {
name
key
default_role
}
}
}
Create a temporary API key with expiration:
mutation {
core {
insert_api_keys(data: {
name: "temp_integration_key"
description: "Temporary key for external integration"
key: "temp_key_xyz789"
default_role: "integration"
is_temporal: true
expires_at: "2024-12-31T23:59:59Z"
}) {
name
key
expires_at
}
}
}
Headers vs Claims
Managed API keys support two ways to provide user information:
1. Headers - Extract user info from request headers:
mutation {
core {
insert_api_keys(data: {
name: "partner_api_key"
description: "Partner API with custom headers"
key: "partner_key_abc123"
default_role: "partner"
headers: {
role: "x-partner-role"
user_id: "x-partner-id"
user_name: "x-partner-name"
}
}) {
name
key
}
}
}
Usage with custom headers:
curl -X POST https://api.example.com/graphql \
-H "Authorization: Bearer partner_key_abc123" \
-H "x-partner-role: premium_partner" \
-H "x-partner-id: partner_123" \
-H "x-partner-name: Acme Corp" \
-d '{"query": "{ customers { id name } }"}'
The role from the header (x-partner-role) takes precedence over default_role if provided.
2. Claims - Use predefined claims from the API key configuration:
mutation {
core {
insert_api_keys(data: {
name: "service_key"
description: "Service account with fixed identity"
key: "service_key_fixed_abc123"
default_role: "service"
claims: {
role: "data_processor"
user_id: "svc_processor_001"
user_name: "Data Processing Service"
department: "engineering"
region: "us-west"
}
}) {
name
key
}
}
}
Usage with claims (no additional headers needed):
curl -X POST https://api.example.com/graphql \
-H "Authorization: Bearer service_key_fixed_abc123" \
-d '{"query": "{ customers { id name } }"}'
Claims are fixed in the database and cannot be overridden by request headers. This is useful for:
- Service accounts with fixed identity
- Keys with predefined permissions
- Additional custom claims for permission filters
Managing API Keys
List all API keys:
query {
core {
api_keys {
name
description
key
default_role
disabled
is_temporal
expires_at
}
}
}
Find key by value:
query {
core {
api_keys_by_key(key: "service_key_fixed_abc123") {
name
description
default_role
disabled
expires_at
}
}
}
Disable an API key without deleting:
mutation {
core {
update_api_keys(
filter: { name: { eq: "mobile_app_key" } }
data: { disabled: true }
) {
success
affected_rows
}
}
}
Update key expiration:
mutation {
core {
update_api_keys(
filter: { name: { eq: "temp_integration_key" } }
data: {
expires_at: "2025-03-31T23:59:59Z"
}
) {
success
}
}
}
Rotate an API key:
mutation {
core {
update_api_keys(
filter: { name: { eq: "partner_api_key" } }
data: {
key: "partner_key_new_xyz789"
}
) {
success
}
}
}
Delete an API key:
mutation {
core {
delete_api_keys(
filter: { name: { eq: "old_key" } }
) {
success
affected_rows
}
}
}
Temporal Keys and Expiration
Temporal keys automatically expire after the specified date:
mutation {
core {
insert_api_keys(data: {
name: "demo_key"
description: "30-day demo access"
key: "demo_abc123"
default_role: "demo"
is_temporal: true
expires_at: "2024-02-15T00:00:00Z"
}) {
name
expires_at
}
}
}
After expiration:
- The key will be rejected during authentication
- The key remains in the database for audit purposes
- You can extend expiration by updating
expires_at
Best Practices for Managed API Keys
- Use Descriptive Names: Name keys after their purpose or owner
- Set Expiration for Temporary Access: Always use
is_temporal: trueandexpires_atfor temporary keys - Use Claims for Service Accounts: Service accounts should use
claimsfor fixed identity - Use Headers for Multi-tenant Keys: When one key serves multiple users, use
headersto identify them - Rotate Keys Regularly: Update the
keyfield periodically for security - Disable Before Delete: Set
disabled: truefirst to test impact before deletion - Audit Key Usage: Keep descriptions updated and monitor key usage
- Combine with Permissions: Use role-based permissions to control what each key can access
Static vs Managed Keys Comparison
| Feature | Static Keys | Managed Keys |
|---|---|---|
| Configuration | YAML/JSON file | Database table |
| Changes | Requires restart | Dynamic, no restart |
| Management | Manual file editing | GraphQL mutations |
| Expiration | Not supported | Built-in with expires_at |
| Audit Trail | Git history | Database history |
| Custom Claims | Fixed in config | Flexible via claims field |
| Use Case | Core system services | User-managed keys, integrations |
Role Assignment
For API keys, the role is determined by:
- Headers - If
headers.roleis configured and the header is present in request - Claims - If
claims.roleis set in the API key - Default Role - From
default_rolefield or static configuration
Authentication Variables Available:
[$auth.user_name]- From headers, claims, or configuration[$auth.user_id]- From headers, claims, or configuration[$auth.role]- From headers, claims, or configuration[$auth.auth_type]- "apikey"- Custom claims from
claimsfield:[$auth.custom_claim_name]
Anonymous Access
Anonymous access allows unauthenticated requests with limited permissions.
Configuration
auth:
anonymous:
enabled: true
role: "public"
Role Assignment
All unauthenticated requests are assigned the role specified in the anonymous.role configuration (typically "public").
Authentication Variables Available:
[$auth.user_name]- "anonymous"[$auth.user_id]- null or "anonymous"[$auth.role]- From configuration (e.g., "public")[$auth.auth_type]- "anonymous"
OAuth2/JWT Authentication
JWT tokens provide stateless authentication with embedded claims.
Configuration
auth:
jwt:
enabled: true
secret: "your-jwt-secret"
algorithm: "HS256" # or RS256, ES256
issuer: "https://your-auth-provider.com"
audience: "your-api-audience"
role_claim: "role" # JWT claim containing the role
role_scope_prefix: "role:" # Prefix for role in scopes
Token Example
{
"sub": "user123",
"name": "John Doe",
"email": "john@example.com",
"role": "editor",
"scope": "read write",
"iss": "https://your-auth-provider.com",
"aud": "your-api-audience",
"exp": 1735689600
}
Usage
Include the JWT token in the Authorization header:
curl -X POST https://api.example.com/graphql \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"query": "{ customers { id name } }"}'
Role Assignment
Roles can be assigned from JWT in two ways:
1. From Claims
The role is read from a specific claim in the JWT payload:
auth:
jwt:
role_claim: "role" # Reads from token.role
If the claim contains multiple roles (array), the first role is used, or you can configure priority:
auth:
jwt:
role_claim: "roles"
role_priority: ["admin", "editor", "viewer"] # First matching role is used
2. From Scopes
The role is extracted from OAuth2 scopes with a specific prefix:
auth:
jwt:
role_scope_prefix: "role:" # Extracts "editor" from scope "role:editor"
Token with scopes:
{
"scope": "read write role:editor"
}
Authentication Variables Available:
[$auth.user_name]- From "name" claim or "sub" claim[$auth.user_id]- From "sub" claim[$auth.role]- From configured role claim or scope[$auth.auth_type]- "jwt"[$auth.provider]- From "iss" claim- Any custom claim:
[$auth.custom_claim]
OIDC Authentication
OpenID Connect extends OAuth2 with standardized identity information.
Configuration
auth:
oidc:
enabled: true
issuer: "https://accounts.google.com"
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://your-app.com/auth/callback"
role_claim: "role"
role_scope_prefix: "role:"
additional_scopes: ["email", "profile"]
Token Example
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"azp": "your-client-id",
"aud": "your-client-id",
"iat": 1735689000,
"exp": 1735692600,
"email": "john@example.com",
"email_verified": true,
"name": "John Doe",
"picture": "https://...",
"role": "editor"
}
Usage
OIDC typically uses the OAuth2 authorization code flow. Include the ID token in requests:
curl -X POST https://api.example.com/graphql \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"query": "{ customers { id name } }"}'
Role Assignment
Similar to JWT, roles can be assigned from:
1. From Claims
auth:
oidc:
role_claim: "role" # Reads from ID token role claim
Common enterprise claims:
role- Custom role claimgroups- Group membership (can be mapped to roles)realm_access.roles- Keycloak-specific rolesresource_access.{client}.roles- Keycloak client-specific roles
2. From Scopes
auth:
oidc:
role_scope_prefix: "role:"
Authentication Variables Available:
[$auth.user_name]- From "name" or "preferred_username" claim[$auth.user_id]- From "sub" claim[$auth.role]- From configured role claim or scope[$auth.auth_type]- "oidc"[$auth.provider]- From "iss" claim[$auth.email]- From "email" claim- Any custom claim:
[$auth.custom_claim]
Multiple Authentication Methods
You can enable multiple authentication methods simultaneously. Hugr tries each method in order:
auth:
# Try API key first
api_keys:
enabled: true
# ... config
# Then OIDC
oidc:
enabled: true
# ... config
# Then JWT
jwt:
enabled: true
# ... config
# Finally anonymous
anonymous:
enabled: true
role: "public"
The first successful authentication method is used. If all fail, the request is treated as anonymous (if enabled).
Custom Claims Mapping
You can map custom claims to authentication variables for use in permissions:
auth:
jwt:
enabled: true
secret: "your-secret"
custom_claims:
tenant_id: "organization_id" # Maps JWT "organization_id" to $auth.tenant_id
department_id: "dept" # Maps JWT "dept" to $auth.department_id
region: "user_region" # Maps JWT "user_region" to $auth.region
These custom claims can then be used in permission filters:
mutation {
core {
insert_role_permissions(data: {
role: "employee"
type_name: "documents"
field_name: "*"
filter: {
department_id: { eq: "[$auth.department_id]" }
}
}) {
role
type_name
}
}
}
Testing Authentication
Test API Key
# Should succeed with configured permissions
curl -X POST http://localhost:8080/graphql \
-H "Authorization: Bearer service_key_abc123" \
-H "X-API-Username: api_service" \
-H "X-API-User-ID: svc_001" \
-d '{"query": "{ customers { id name } }"}'
Test Anonymous
# Should succeed with public role permissions
curl -X POST http://localhost:8080/graphql \
-d '{"query": "{ customers { id name } }"}'
Test JWT
# Generate a test token (example using jwt.io or similar tool)
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -X POST http://localhost:8080/graphql \
-H "Authorization: Bearer $TOKEN" \
-d '{"query": "{ customers { id name } }"}'
Performance: Role Permission Caching
Hugr automatically caches role permissions to optimize authorization performance. Understanding how this works helps ensure your authentication changes take effect immediately.
How It Works
When a user authenticates and their role is determined:
- First Request: Fetch role permissions from database (50-100ms)
- Cache Result: Store with key
RolePermissions:{role_name}and tag$role_permissions - Subsequent Requests: Serve from cache (1-5ms)
- Cache Lifetime: Typically 1 hour (configurable)
Performance Impact
- 90%+ reduction in database queries for permission checks
- 10-20x faster authorization decisions (5ms vs 50-100ms)
- Scales to thousands of concurrent users without database bottleneck
Invalidating Cache After Role Changes
When you modify roles or permissions, invalidate the cache to ensure users get updated permissions:
mutation UpdateRoleAndRefresh {
core {
# Update role permissions
update_role_permissions(
filter: { role: { eq: "editor" } }
data: { disabled: false }
) {
success
}
# Invalidate permission cache
invalidateCache: function {
core {
cache {
invalidate(tags: ["$role_permissions"]) {
success
affected_rows
}
}
}
}
}
}
Always invalidate cache when:
- Creating or modifying API keys with different roles
- Updating JWT/OIDC role mappings
- Changing role assignments
- Modifying permission rules
For more details, see:
Security Best Practices
- Use HTTPS: Always use TLS/SSL in production
- Rotate Keys: Regularly rotate API keys and JWT secrets
- Limit Token Lifetime: Use short-lived tokens with refresh mechanisms
- Validate Issuers: Always verify token issuer in production
- Principle of Least Privilege: Assign minimal necessary roles
- Invalidate Permission Cache: After role changes, clear cached permissions
- Audit Logs: Enable logging for authentication events
- Rate Limiting: Implement rate limiting to prevent abuse
- Secure Storage: Never commit secrets to version control
Troubleshooting
Authentication Failed
- Check token is not expired
- Verify token signature with correct secret/public key
- Confirm issuer and audience match configuration
- Check Authorization header format:
Bearer <token>
Role Not Assigned
- Verify role claim name matches configuration
- Check token contains the expected role claim
- Confirm role exists in the roles table
- Review role assignment configuration
Permission Denied
- Confirm user has correct role assigned
- Check role has necessary permissions in access control
- Verify permissions are not disabled
- Test with admin role to isolate permission vs. auth issues
Custom Claims Not Available
- Verify claim exists in token payload
- Check custom_claims mapping in configuration
- Ensure claim name matches exactly (case-sensitive)
- Test with a tool like jwt.io to inspect token
Integration Examples
Auth0
auth:
oidc:
enabled: true
issuer: "https://your-tenant.auth0.com/"
client_id: "your-client-id"
client_secret: "your-client-secret"
role_claim: "https://your-app.com/roles" # Custom namespace claim
Keycloak
auth:
oidc:
enabled: true
issuer: "https://keycloak.example.com/realms/your-realm"
client_id: "your-client-id"
client_secret: "your-client-secret"
role_claim: "realm_access.roles"
Google
auth:
oidc:
enabled: true
issuer: "https://accounts.google.com"
client_id: "your-client-id.apps.googleusercontent.com"
client_secret: "your-client-secret"
additional_scopes: ["email", "profile"]
# Google doesn't provide roles by default, use custom solution
Azure AD
auth:
oidc:
enabled: true
issuer: "https://login.microsoftonline.com/{tenant-id}/v2.0"
client_id: "your-client-id"
client_secret: "your-client-secret"
role_claim: "roles" # Azure AD app roles
See Also
- Access Control - Configure roles and permissions
- Cache Directives - Role permission caching
- Configuration - General Hugr configuration
- Deployment - Deploy Hugr with authentication
- Hugr Repository - Detailed configuration examples