Konnektr Logo
Advanced Topics

Authentication & Authorization

How to authenticate and manage access to Konnektr Graph

Konnektr Graph uses OAuth 2.1 with JWT Bearer tokens for authentication and fine-grained, permission-based authorization for access control.

Client Credentials

Konnektr Graph validates OAuth 2.1 JWT bearer tokens issued by the identity provider you configure for your deployment. Every caller needs a client registration in that provider.

Getting Your Credentials

Register a client application in your identity provider. Your credentials include:

  • Client ID - Identifies your application
  • Client Secret - Secret key for authentication
  • Audience - The API identifier (e.g., api://konnektr-graph)
  • Authority - The OAuth server URL (e.g., https://auth.example.com)

Authenticating Requests

Use OAuth 2.1 Client Credentials flow to get an access token:

curl -X POST https://auth.example.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "audience": "api://konnektr-graph",
    "grant_type": "client_credentials"
  }'

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 86400
}

Use the access token in all API requests:

curl https://graph.example.com/digitaltwins/twin-1 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Using Azure Digital Twins SDKs

You can use the standard Azure Digital Twins SDKs with Konnektr Graph:

# Python
from azure.digitaltwins.core import DigitalTwinsClient
from azure.core.credentials import AccessToken
from datetime import datetime
import requests

# Get token from your OAuth provider
token_response = requests.post(
    "https://auth.example.com/oauth/token",
    json={
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
        "audience": "api://konnektr-graph",
        "grant_type": "client_credentials"
    }
)
access_token = token_response.json()["access_token"]

# Create custom credential
class StaticTokenCredential:
    def __init__(self, token):
        self.token = token
    
    def get_token(self, *scopes, **kwargs):
        expires_on = int(datetime.now().timestamp()) + 3600
        return AccessToken(self.token, expires_on)

# Connect to Konnektr Graph
client = DigitalTwinsClient(
    "https://graph.example.com",
    StaticTokenCredential(access_token)
)

# Use the client
twin = client.get_digital_twin("my-twin-id")

Managing Permissions

Permissions are either embedded in the token your identity provider issues, or fetched per request from a permission API you run. See Permission Providers for both options.

Common Permissions:

PermissionDescription
digitaltwins/readRead digital twins and components
digitaltwins/writeCreate and update digital twins
digitaltwins/deleteDelete digital twins
digitaltwins/relationships/readRead relationships
digitaltwins/relationships/writeCreate and update relationships
digitaltwins/relationships/deleteDelete relationships
models/readRead DTDL models
models/writeCreate and update models
query/readExecute Cypher queries

Identity Provider Configuration

Konnektr Graph does not ship an identity provider. Point it at your own OAuth 2.1 / OpenID Connect provider.

Supported Providers

Konnektr Graph works with any OAuth 2.1 / OpenID Connect provider:

  • Auth0
  • Azure AD / Microsoft Entra ID
  • Keycloak
  • Okta
  • Custom OAuth 2.1 server

Configuration

Set these environment variables or update appsettings.json:

API Service:

# Enable authentication
Authentication__Enabled=true
Authentication__Authority=https://auth.example.com
Authentication__Audience=api://konnektr-graph
Authentication__Issuer=https://auth.example.com/

# Enable authorization
Authorization__Enabled=true
Authorization__Provider=Claims
Authorization__PermissionsClaimName=permissions

JWT Token Requirements

Your OAuth provider must issue JWT tokens with these claims:

{
  "sub": "user-or-service-account-id",
  "aud": "api://konnektr-graph",
  "iss": "https://auth.example.com/",
  "scope": "digitaltwins:read",
  "permissions": [
    "digitaltwins/read",
    "digitaltwins/write",
    "digitaltwins/read"
  ]
}

Required Claims:

  • sub - Subject (user/service account identifier)
  • aud - Audience (must match your configured audience)
  • iss - Issuer (must match your configured issuer)

Optional but Recommended:

  • scope - OAuth scopes required by your API (e.g. digitaltwins:read)
  • permissions - Array of permission strings

Permission Providers

You can choose how permissions are provided:

1. JWT Claims (Default)

Permissions are embedded in the JWT token's permissions claim:

{
  "Authorization": {
    "Provider": "Claims",
    "PermissionsClaimName": "permissions"
  }
}

Pros:

  • Fast (no external calls)
  • Works offline
  • Simple setup

Cons:

  • Permissions baked into token (can't revoke until expiry)
  • Larger token size

2. API Provider

Fetch permissions from an external API on each request:

{
  "Authorization": {
    "Provider": "Api",
    "ApiProvider": {
      "BaseUrl": "https://your-permission-api.com",
      "CheckEndpoint": "/api/v1/permissions/check",
      "ResourceName": "graph-instance-123",
      "CacheExpirationMinutes": 5,
      "TimeoutSeconds": 10
    }
  }
}

Pros:

  • Dynamic permissions (can revoke immediately)
  • Centralized permission management
  • Smaller token size

Cons:

  • Requires external API call
  • Potential latency
  • Dependency on external service

Proxy Delegation

When the API service runs behind an authenticating proxy that uses a managed identity to forward requests, the delegated user ID from an HTTP header can be used instead of the managed identity's sub claim. This is configured via:

{
  "Parameters": {
    "UserIdHeaderName": "X-User-Id",
    "UserIdHeaderRequired": false
  }
}

When enabled, the UserIdHeaderMiddleware reads the header value (e.g., a B2C user ID injected by the proxy) and creates a synthetic ClaimsIdentity prepended to the ClaimsPrincipal. All downstream authorization checks and TrackLastUpdatedBy use this synthetic identity.

For the Api permission provider, the synthetic identity's sub claim (from the header) is used for permission lookups. For the Claims provider, original managed identity permission claims are preserved and accessible.

See Security for details.

Configuring Your Auth Provider

Auth0 Example

  1. Create an API in Auth0 Dashboard

    • Name: Konnektr Graph
    • Identifier: api://konnektr-graph
  2. Create a Machine-to-Machine Application

    • Authorize it for your API
    • Note the Client ID and Secret
  3. Add Permissions (in API settings)

    • Add each permission as a scope (e.g., digitaltwins:read, models/read)
  4. Configure Rules (optional)

    • Add custom claims to include permissions array

Azure AD Example

  1. Register an Application

    • Note the Application (client) ID
    • Create a client secret
  2. Expose an API

    • Set Application ID URI: api://konnektr-graph
    • Add scopes for each permission
  3. App Roles (optional)

    • Define app roles for permission groups
    • Assign users/service principals to roles
  4. Token Configuration

    • Add optional claims to include permissions

OAuth Discovery

The API service exposes an OAuth discovery endpoint at:

/.well-known/oauth-protected-resource

This endpoint returns RFC 9728 Protected Resource Metadata:

{
  "resource": "https://your-server.com",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["digitaltwins:read", "digitaltwins:write"],
  "bearer_methods_supported": ["header"]
}

Error Responses

Understanding error responses helps troubleshoot authentication and authorization issues.

401 Unauthorized

Returned when no valid JWT token is provided:

{
  "error": "unauthorized",
  "error_description": "Authentication required. Please provide a valid Bearer token.",
  "resource_metadata": "https://your-server/.well-known/oauth-protected-resource"
}

HTTP Headers:

WWW-Authenticate: Bearer realm="graph", resource_metadata="https://..."

Common Causes:

  • Missing Authorization header
  • Expired token
  • Invalid token signature
  • Token from wrong issuer

Solution: Get a fresh access token from your OAuth provider.

403 Forbidden - Missing Scope

Returned when OAuth scope is missing:

{
  "error": "insufficient_scope",
  "error_description": "Required scopes: digitaltwins:read",
  "scope": "digitaltwins:read"
}

Common Causes:

  • Token doesn't include required OAuth scope
  • Scope not configured in OAuth provider

Solution:

  • Configure your OAuth provider to include the scope in issued tokens

403 Forbidden - Missing Permission

Returned when permission is missing:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.3",
  "title": "Forbidden",
  "status": 403,
  "detail": "User lacks required permission: digitaltwins/read"
}

Common Causes:

  • User/service account doesn't have required permission
  • Permission not included in JWT token
  • Permission API call failed (if using API provider)

Solution:

  • Add the permission to the JWT token, or configure your permission API to return it

Troubleshooting

Debugging Authentication

Check your token:

# Decode JWT (using jwt.io or jwt-cli)
echo "YOUR_TOKEN" | jwt decode -

# Verify claims:
# - aud: Must match your configured audience
# - iss: Must match your configured issuer
# - exp: Must be in the future
# - permissions: Must include required permissions

Test authentication:

# Try accessing a public endpoint
curl https://your-server/.well-known/oauth-protected-resource

# Try with your token
curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://your-server/digitaltwins/twin-1

Common Issues

Issue: "401 Unauthorized" with valid token

Possible causes:

  • Token audience doesn't match configured audience
  • Token issuer doesn't match configured issuer
  • Clock skew (token not yet valid or expired)

Solution: Check your Authentication:Audience and Authentication:Issuer configuration.

Issue: "403 Forbidden" for all requests

Possible causes:

  • Permissions not configured
  • Wrong permission claim name
  • Permission provider not working

Solution:

  1. Check Authorization:PermissionsClaimName matches your token
  2. Verify permissions are in the token
  3. Check logs for permission provider errors

Enabling Debug Logging

Enable verbose authentication logging:

Add to appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore.Authentication": "Debug",
      "Microsoft.AspNetCore.Authorization": "Debug",
      "AgeDigitalTwins.ServiceDefaults.Authorization": "Debug"
    }
  }
}

This logs:

  • JWT validation steps
  • Permission checks
  • Authorization decisions

Security Best Practices

Token Management

  • Rotate secrets regularly - Change client secrets every 90 days
  • Use short token expiry - Keep tokens valid for 1 hour or less
  • Secure token storage - Never commit tokens to source control
  • Use HTTPS only - Always use encrypted connections

Permission Design

  • Principle of least privilege - Grant minimum permissions needed
  • Separate read/write - Don't use wildcards unless necessary
  • Audit permissions - Review access regularly
  • Use service accounts - Dedicated credentials for applications

Operational Considerations

  • Secure OAuth provider - Use established providers (Auth0, Azure AD)
  • Monitor auth logs - Watch for suspicious patterns
  • Implement rate limiting - Prevent brute force attacks
  • Keep software updated - Apply security patches promptly

Further Reading

On this page