Service Accounts (M2M)
Using machine-to-machine authentication to check permissions on behalf of users
Service Accounts for Permission Checking
Overview
Service accounts allow your backend services to authenticate with KtrlPlane and check user permissions on behalf of those users. This is useful when:
- Your service has a different Auth0 audience than KtrlPlane
- You need to verify user permissions without forwarding user tokens
- Auth0's on-behalf-of flow isn't available for your use case
How It Works
- M2M Application: Create an Auth0 Machine-to-Machine application
- Client Credentials Flow: Your service authenticates using client ID and secret
- Special Permission: Grant the M2M app permission to check permissions on behalf of users
- API Call: Call
/api/v1/permissions/checkwith auserIdparameter
Setup Guide
Step 1: Create Auth0 M2M Application
- Go to your Auth0 Dashboard → Applications → Create Application
- Choose "Machine to Machine Applications"
- Name it (e.g., "My Service - KtrlPlane Integration")
- Select the KtrlPlane API as the authorized API
- Grant the necessary permissions (the API must be configured to allow M2M access)
- Note the Client ID and Client Secret
Step 2: Grant Service Account Permission
The service account needs a special permission to check permissions on behalf of users. You'll need to create a role assignment at the global scope.
Option A: Using SQL (Recommended for initial setup)
-- 1. Find or create a role with the special permission
INSERT INTO ktrlplane.roles (role_id, name, description, scope_type, display_order, created_at, updated_at)
VALUES (
'service-account-permission-checker',
'Service Account: Permission Checker',
'Allows service accounts to check permissions on behalf of users',
'global',
1000,
NOW(),
NOW()
)
ON CONFLICT (role_id) DO NOTHING;
-- 2. Add the check_permissions_on_behalf_of permission to the role
INSERT INTO ktrlplane.role_permissions (role_id, permission_id)
VALUES ('service-account-permission-checker', '00000000-0001-0000-0000-000000000006')
ON CONFLICT DO NOTHING;
-- 3. Assign the role to your service account (use the client ID as user_id)
INSERT INTO ktrlplane.role_assignments (
assignment_id,
user_id, -- This is the Auth0 client ID (the 'sub' from the M2M token)
role_id,
scope_type,
scope_id,
assigned_by,
created_at,
updated_at
)
VALUES (
gen_random_uuid(),
'YOUR_M2M_CLIENT_ID_HERE', -- Replace with your M2M application's client ID
'service-account-permission-checker',
'global',
'global',
'system', -- Or use your admin user ID
NOW(),
NOW()
);Option B: Using the API (Future Enhancement)
Currently, service account role assignments must be created via SQL. In the future, an API endpoint will be available for this.
Step 3: Authenticate Your Service
Your service needs to obtain an access token using the client credentials flow:
// Example using Node.js
import axios from 'axios';
async function getServiceAccountToken() {
const response = await axios.post(`https://YOUR_AUTH0_DOMAIN/oauth/token`, {
grant_type: 'client_credentials',
client_id: process.env.M2M_CLIENT_ID,
client_secret: process.env.M2M_CLIENT_SECRET,
audience: process.env.KTRLPLANE_API_AUDIENCE,
});
return response.data.access_token;
}// Example using Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
func getServiceAccountToken(auth0Domain, clientID, clientSecret, audience string) (string, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")
data.Set("client_id", clientID)
data.Set("client_secret", clientSecret)
data.Set("audience", audience)
req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/oauth/token", auth0Domain),
strings.NewReader(data.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", err
}
return tokenResp.AccessToken, nil
}Step 4: Check Permissions on Behalf of Users
Now your service can check permissions for any user:
// Example using Node.js
async function checkUserPermissions(
userId: string,
scopeType: string,
scopeId: string
) {
const token = await getServiceAccountToken();
const response = await axios.get(
`${process.env.KTRLPLANE_API_URL}/api/v1/permissions/check`,
{
params: {
userId: userId, // The user you're checking permissions for
scopeType: scopeType, // e.g., "project", "organization", "resource"
scopeId: scopeId, // The specific project/org/resource ID
},
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return response.data.permissions; // Array of permission strings
}
// Usage example
const permissions = await checkUserPermissions(
'auth0|123456789', // User's Auth0 sub claim
'project',
'my-project-id'
);
if (permissions.includes('read')) {
// User can read the project
}// Example using Go
func checkUserPermissions(token, userID, scopeType, scopeID string) ([]string, error) {
url := fmt.Sprintf("%s/api/v1/permissions/check?userId=%s&scopeType=%s&scopeId=%s",
os.Getenv("KTRLPLANE_API_URL"),
url.QueryEscape(userID),
url.QueryEscape(scopeType),
url.QueryEscape(scopeID),
)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Permissions []string `json:"permissions"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Permissions, nil
}API Reference
Check Permissions
Endpoint: GET /api/v1/permissions/check
Authentication: Bearer token (user token or service account token)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
scopeType | string | Yes | Type of scope: "organization", "project", "resource" |
scopeId | string | Yes | ID of the specific scope |
userId | string | No* | User ID to check permissions for (M2M only) |
*Required when using service account to check another user's permissions
Response:
{
"user_id": "auth0|123456789",
"scope_type": "project",
"scope_id": "my-project-id",
"permissions": [
"read",
"write",
"manage_access"
]
}Error Responses:
400 Bad Request: Missing required parameters401 Unauthorized: Invalid or missing token403 Forbidden: Service account lacks permission to check on behalf of users500 Internal Server Error: Server-side error
Security Considerations
Why This Is Safe
- Explicit Permission Required: Service accounts must be explicitly granted the
check_permissions_on_behalf_ofpermission - Global Scope Only: This permission is only checked at the global scope, making it easy to audit
- Read-Only Operation: Service accounts can only check permissions, not grant or modify them
- Audit Trail: All permission checks are logged (if logging is enabled)
Best Practices
- Limit Service Accounts: Only create service accounts when necessary
- Rotate Credentials: Regularly rotate client secrets
- Use Environment Variables: Never hardcode credentials
- Monitor Usage: Set up alerts for unusual permission check patterns
- Principle of Least Privilege: Only grant the specific permission needed
What Service Accounts CANNOT Do
Service accounts with check_permissions_on_behalf_of permission:
- ❌ Cannot modify user permissions or role assignments
- ❌ Cannot create, update, or delete resources
- ❌ Cannot impersonate users for other API calls
- ❌ Cannot access user data beyond permission information
- ✅ Can only read what permissions a user has on a specific scope
Troubleshooting
"Only service accounts can check permissions on behalf of other users"
Cause: You're using a regular user token, not an M2M token.
Solution: Ensure you're authenticating with client credentials flow, not a user token.
"Service account does not have permission to check permissions on behalf of users"
Cause: The service account hasn't been granted the check_permissions_on_behalf_of permission.
Solution: Follow Step 2 in the setup guide to create the role assignment.
How to Find Your M2M Client ID
The client ID becomes the sub claim in the M2M token. To verify:
- Decode your M2M token at jwt.io
- Look for the
subclaim - this is your client ID - Verify the
gtyclaim is"client-credentials"
Example token payload:
{
"iss": "https://your-tenant.auth0.com/",
"sub": "AbC123xyz@clients", // This is your client ID
"aud": "https://api.ktrlplane.io",
"gty": "client-credentials", // Identifies this as M2M token
"azp": "AbC123xyz",
"exp": 1234567890,
"iat": 1234567890
}Example: Integration with Konnektr.Graph
Here's a real-world example of how Konnektr.Graph uses service accounts to verify user permissions:
// In Konnektr.Graph API middleware
async function verifyUserCanAccessGraph(userId: string, graphResourceId: string) {
// Get service account token
const token = await getServiceAccountToken();
// Check if user has read permission on the graph resource
const permissions = await checkUserPermissions(
userId,
'resource',
graphResourceId
);
if (!permissions.includes('read')) {
throw new UnauthorizedError('User does not have access to this graph');
}
// Permission verified, proceed with graph operation
return true;
}