headscale/docs/ref/api-groups.md
Ryan Malloy 5abc3c87b2 OIDC groups implementation
- Add Groups field to User struct with JSON storage
- Include GetGroups() and SetGroups() helper methods
- Extract groups from OIDC claims in FromClaim()
- Add database migration 202509161200 for groups column
- Update config-example.yaml with groups scope
- Add comprehensive documentation and testing
2026-05-21 17:55:31 -06:00

7.6 KiB

API Reference: OIDC Groups

This document describes the API changes related to OIDC group storage and management introduced in Headscale v0.24.0.

Overview

Headscale now stores OIDC group membership information extracted from authentication claims. This enables external integrations to implement role-based access control based on a user's group membership.

User API Changes

User Object Schema

The User object now includes group information when users authenticate via OIDC:

{
  "id": "1",
  "name": "alice",
  "createdAt": "2024-01-01T00:00:00Z",
  "displayName": "Alice Smith",
  "email": "alice@example.com",
  "providerId": "https://provider.com/alice",
  "provider": "oidc",
  "profilePicUrl": "https://provider.com/avatar/alice.jpg",
  "groups": ["admin", "developers", "security-team"]
}

Field Descriptions

Field Type Description
groups string[] Array of group names extracted from OIDC claims. Empty array for non-OIDC users.

API Endpoints

List Users

Returns all users including their group membership.

Request:

GET /api/v1/user
Authorization: Bearer <api-key>

Response:

{
  "users": [
    {
      "id": "1",
      "name": "alice",
      "email": "alice@example.com",
      "provider": "oidc",
      "groups": ["admin", "developers"]
    },
    {
      "id": "2", 
      "name": "bob",
      "email": "",
      "provider": "cli",
      "groups": []
    }
  ]
}

Get User

Returns a specific user including group membership.

Request:

GET /api/v1/user/{name}
Authorization: Bearer <api-key>

Response:

{
  "user": {
    "id": "1",
    "name": "alice",
    "email": "alice@example.com",
    "provider": "oidc",
    "groups": ["admin", "developers", "security-team"]
  }
}

Group Management

Automatic Group Updates

Groups are automatically updated when OIDC users authenticate:

  1. User logs in via OIDC
  2. Groups are extracted from ID token and/or UserInfo endpoint
  3. User's group membership is updated in the database
  4. API responses include updated group information

Group Sources

Groups can be extracted from multiple sources in OIDC claims:

  • Standard groups claim: Most common format
  • roles claim: Alternative role-based claim
  • Provider-specific claims: e.g., cognito:groups for AWS Cognito
  • Nested claims: e.g., resource_access.client.roles for Keycloak

Group Validation

  • Only string values are accepted as group names
  • Empty or null groups are filtered out
  • Group names are stored as-is (case-sensitive)
  • Maximum reasonable limit of groups per user (no strict limit enforced)

Integration Examples

Role-Based Access Control

External applications can query user groups for access control:

# Get user groups via API
curl -H "Authorization: Bearer $API_KEY" \
  https://headscale.example.com/api/v1/user/alice | \
  jq '.user.groups[]'

# Check if user has admin group
curl -H "Authorization: Bearer $API_KEY" \
  https://headscale.example.com/api/v1/user/alice | \
  jq '.user.groups | contains(["admin"])'

Database Queries

For direct database access:

-- Get all users with their groups
SELECT name, email, groups FROM users WHERE provider = 'oidc';

-- Find users in specific group
SELECT name FROM users 
WHERE provider = 'oidc' 
AND JSON_EXTRACT(groups, '$') LIKE '%"admin"%';

-- Count users by group membership
SELECT 
  json_each.value as group_name,
  COUNT(*) as user_count
FROM users, json_each(users.groups)
WHERE provider = 'oidc'
GROUP BY json_each.value;

WebUI Integration

Headplane Integration

When using Headplane as a web interface, the groups information enables:

  • Automatic Role Assignment: Map OIDC groups to Headplane roles
  • Dynamic Permissions: Update user capabilities based on current group membership
  • Audit Trails: Track role assignments based on group changes

Example Headplane API usage:

// Fetch user with groups
const response = await fetch('/api/v1/user/alice', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
});
const user = await response.json();

// Map groups to roles
const roles = mapGroupsToRoles(user.groups);
console.log(`User ${user.name} has roles:`, roles);

Backward Compatibility

CLI Users

  • Users created via CLI (headscale users create) have empty groups array
  • No changes to existing CLI user management
  • Groups field is optional and defaults to empty array

API Compatibility

  • All existing API endpoints continue to work unchanged
  • New groups field is additive (doesn't break existing clients)
  • Clients can safely ignore the groups field if not needed

Migration

  • Existing OIDC users will have empty groups until next login
  • No database migration required for basic functionality
  • Groups are populated automatically on subsequent OIDC logins

Configuration

Headscale Configuration

Ensure groups scope is included for group extraction:

oidc:
  issuer: "https://your-provider.com"
  client_id: "headscale"
  client_secret: "your-secret"
  scope: ["openid", "profile", "email", "groups"]

Provider-Specific Configuration

=== "Keycloak" yaml # Keycloak automatically includes groups in standard format oidc: scope: ["openid", "profile", "email", "groups"]

=== "Azure AD" yaml # Azure AD requires group claims configuration oidc: scope: ["openid", "profile", "email"] # Groups included via claims configuration in Azure AD

=== "Okta" yaml # Okta supports groups in tokens oidc: scope: ["openid", "profile", "email", "groups"]

Error Handling

Common Issues

Groups not appearing:

  • Verify groups scope is included in OIDC configuration
  • Check identity provider group claim configuration
  • Ensure user is member of groups in identity provider

Invalid group data:

  • Non-string group values are automatically filtered out
  • Empty arrays are valid (user has no groups)
  • Malformed JSON is handled gracefully with empty array fallback

Error Responses

Standard HTTP error codes apply:

  • 401 Unauthorized: Invalid or missing API key
  • 404 Not Found: User does not exist
  • 500 Internal Server Error: Server-side processing error

Security Considerations

Access Control

  • Groups information is available to any client with valid API key
  • Consider creating read-only API keys for external integrations
  • Audit API key usage for compliance requirements

Data Privacy

  • Group names may contain sensitive organizational information
  • Consider data classification for group membership information
  • Implement appropriate access controls for group data

Token Security

  • Groups are extracted from verified OIDC tokens only
  • Token validation ensures groups cannot be spoofed
  • Groups are updated only during successful authentication

Monitoring and Observability

Metrics

Monitor group extraction and updates:

  • Number of OIDC logins with groups extracted
  • Distribution of group membership across users
  • Failed group extractions or parsing errors

Logging

Key log events to monitor:

  • Group extraction from OIDC claims
  • Group updates during user authentication
  • Group parsing errors or validation failures

Example log entries:

INFO  Groups extracted from OIDC claims user=alice groups=["admin","developers"]
WARN  Invalid group value filtered out user=bob value=123 type=number
ERROR Failed to parse groups from OIDC claims user=charlie error="invalid JSON"

This API enhancement provides the foundation for implementing sophisticated role-based access control systems while maintaining backward compatibility with existing deployments.