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

309 lines
7.6 KiB
Markdown

# 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:
```json
{
"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:**
```http
GET /api/v1/user
Authorization: Bearer <api-key>
```
**Response:**
```json
{
"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:**
```http
GET /api/v1/user/{name}
Authorization: Bearer <api-key>
```
**Response:**
```json
{
"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:
```bash
# 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:
```sql
-- 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:
```javascript
// 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:
```yaml
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.