oidc groups: store and expose the OIDC groups claim

Add a Groups column on the User model populated from the OIDC
'groups' claim at login, and surface it through the gRPC/REST
User message so external tools (Headplane) can read group
membership without reaching into the database.

- proto: add 'repeated string groups = 9' to v1.User.
- types.User: Groups text column, GetGroups/SetGroups JSON helpers,
  FromClaim populates from claims.Groups. The existing
  FlexibleStringSlice on OIDCClaims.Groups already handles
  JumpCloud-style single-string emission.
- types.User.Proto(): populate v1.User.Groups via GetGroups().
- db migration 202505141323: add the column BEFORE the existing
  202505141324 migration that loads users via the struct, so the
  schema is in place before any migration touches the User type.
- db migration 202507021200: extend the inline CREATE TABLE users
  and INSERT INTO users ... SELECT FROM users_old to carry the
  new column through the SQLite schema-recreation step.
- schema.sql: declare the column so squibble.Validate accepts
  databases produced by the new migration chain. Verified against
  all 7 historical sqlite dumps in hscontrol/db/testdata/sqlite.
- types.UserView, types_clone.go: regenerated to expose Groups.
- config-example.yaml, docs/ref/oidc.md: note the 'groups' scope
  and the role the column plays for external integrations.
- integration/oidc_groups_test.go: verify the round-trip via
  headscale.ListUsers() for users with multi-group, single-group,
  and empty group memberships.
This commit is contained in:
Ryan Malloy 2026-06-04 01:57:50 -06:00
parent 5228cb1a40
commit 209ba5c4ea
12 changed files with 266 additions and 7 deletions

View file

@ -240,13 +240,64 @@ endpoint.
| username | `preferred_username` | Depends on identity provider, eg: `ssmith`, `ssmith@idp.example.com`, `\\example.com\ssmith` |
| profile picture | `picture` | URL to a profile picture or avatar |
| provider identifier | `iss`, `sub` | A stable and unique identifier for a user, typically a combination of `iss` and `sub` OIDC claims |
| | `groups` | [Only used to filter for allowed groups](#authorize-users-with-filters) |
| group membership | `groups` | Used for [access filtering](#authorize-users-with-filters) and stored for external integrations |
## Group Storage and Integration
Starting with Headscale v0.24.0, OIDC group membership is automatically extracted from authentication claims and stored in the database. This enables external integrations (such as web interfaces) to implement role-based access control based on a user's group membership.
### Group Storage
- Groups are extracted from both ID tokens and UserInfo endpoint responses
- Group membership is updated on every successful OIDC login
- Groups are stored as JSON in the user database for external access
- Multiple group claim formats are supported (`groups`, `roles`, provider-specific claims)
### External Integration
External applications can query user group membership for implementing role-based access control:
```bash
# View user groups via Headscale CLI
headscale users list --output json
# Example database query (for direct database access)
SELECT name, email, groups FROM users WHERE provider = 'oidc';
```
### Scope Requirements
To enable group storage, ensure your OIDC configuration includes the `groups` scope:
```yaml
oidc:
issuer: "https://sso.example.com"
client_id: "headscale"
client_secret: "generated-secret"
scope: ["openid", "profile", "email", "groups"]
```
### Headplane Integration
When using [Headplane](https://github.com/tale/headplane) as a web interface for Headscale, OIDC groups enable automatic role-based access control:
- **Automatic Role Assignment**: Users are assigned roles based on their OIDC group membership
- **Zero-Trust Security**: New users receive minimal access until proper groups are assigned
- **Dynamic Updates**: User roles update automatically on each login based on current group membership
- **Configurable Mapping**: Organizations can customize which groups map to which roles
Example Headplane role mapping configuration:
```yaml
role_mapping:
owner: ["ceo", "cto", "headscale-owner"]
admin: ["it-admin", "platform-admin"]
network_admin: ["network-team", "devops"]
auditor: ["compliance", "audit-team"]
```
For detailed Headplane OIDC configuration, see the [Headplane documentation](https://github.com/tale/headplane/docs).
## Limitations
- Support for OpenID Connect aims to be generic and vendor independent. It offers only limited support for quirks of
specific identity providers.
- OIDC groups cannot be used in policy rules.
- OIDC groups cannot be used in policy rules directly (use external integrations for role-based access control).
- The username provided by the identity provider needs to adhere to this pattern:
- The username must be at least two characters long.
- It must only contain letters, digits, hyphens, dots, underscores, and up to a single `@`.