# OpenID Connect

Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features:

- Auto configuration via OpenID Connect Discovery Protocol
- [Proof Key for Code Exchange (PKCE) code verification](#enable-pkce-recommended)
- [Authorization based on a user's domain, email address or group membership](#authorize-users-with-filters)
- Synchronization of [standard OIDC claims](#supported-oidc-claims)

Please see [limitations](#limitations) for known issues and limitations.

## Configuration

OpenID requires configuration in Headscale and your identity provider:

- Headscale: The `oidc` section of the Headscale [configuration](configuration.md) contains all available configuration
  options along with a description and their default values.
- Identity provider: Please refer to the official documentation of your identity provider for specific instructions.
  Additionally, there might be some useful hints in the [Identity provider specific
  configuration](#identity-provider-specific-configuration) section below.

### Basic configuration

A basic configuration connects Headscale to an identity provider and typically requires:

- OpenID Connect Issuer URL from the identity provider. Headscale uses the OpenID Connect Discovery Protocol 1.0 to
  automatically obtain OpenID configuration parameters (example: `https://sso.example.com`).
- Client ID from the identity provider (example: `headscale`).
- Client secret generated by the identity provider (example: `generated-secret`).
- Redirect URI for your identity provider (example: `https://headscale.example.com/oidc/callback`).

=== "Headscale"

    ```yaml
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
    ```

=== "Identity provider"

    * Create a new confidential client (`Client ID`, `Client secret`)
    * Add Headscale's OIDC callback URL as valid redirect URL: `https://headscale.example.com/oidc/callback`
    * Configure additional parameters to improve user experience such as: name, description, logo, …

### Enable PKCE (recommended)

Proof Key for Code Exchange (PKCE) adds an additional layer of security to the OAuth 2.0 authorization code flow by
preventing authorization code interception attacks, see: <https://datatracker.ietf.org/doc/html/rfc7636>. PKCE is
recommended and needs to be configured for Headscale and the identity provider alike:

=== "Headscale"

    ```yaml hl_lines="5-6"
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
      pkce:
        enabled: true
    ```

=== "Identity provider"

    * Enable PKCE for the headscale client
    * Set the PKCE challenge method to "S256"

### Authorize users with filters

Headscale allows to filter for allowed users based on their domain, email address or group membership. These filters can
be helpful to apply additional restrictions and control which users are allowed to join. Filters are disabled by
default, users are allowed to join once the authentication with the identity provider succeeds. In case multiple filters
are configured, a user needs to pass all of them.

=== "Allowed domains"

    * Check the email domain of each authenticating user against the list of allowed domains and only authorize users
      whose email domain matches `example.com`.
    * A verified email address is required [unless email verification is disabled](#control-email-verification).
    * Access allowed: `alice@example.com`
    * Access denied: `bob@example.net`

    ```yaml hl_lines="5-6"
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
      allowed_domains:
        - "example.com"
    ```

=== "Allowed users/emails"

    * Check the email address of each authenticating user against the list of allowed email addresses and only authorize
      users whose email is part of the `allowed_users` list.
    * A verified email address is required [unless email verification is disabled](#control-email-verification).
    * Access allowed: `alice@example.com`, `bob@example.net`
    * Access denied: `mallory@example.net`

    ```yaml hl_lines="5-7"
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
      allowed_users:
        - "alice@example.com"
        - "bob@example.net"
    ```

=== "Allowed groups"

    * Use the OIDC `groups` claim of each authenticating user to get their group membership and only authorize users
      which are members in at least one of the referenced groups.
    * Access allowed: users in the `headscale_users` group
    * Access denied: users without groups, users with other groups

    ```yaml hl_lines="5-7"
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
      scope: ["openid", "profile", "email", "groups"]
      allowed_groups:
        - "headscale_users"
    ```

### Control email verification

Headscale uses the `email` claim from the identity provider to synchronize the email address to its user profile. By
default, a user's email address is only synchronized when the identity provider reports the email address as verified
via the `email_verified: true` claim.

Unverified emails may be allowed in case an identity provider does not send the `email_verified` claim or email
verification is not required. In that case, a user's email address is always synchronized to the user profile.

```yaml hl_lines="5"
oidc:
  issuer: "https://sso.example.com"
  client_id: "headscale"
  client_secret: "generated-secret"
  email_verified_required: false
```

### Customize node expiration

The node expiration is the amount of time a node is authenticated with OpenID Connect until it expires and needs to
reauthenticate. The default node expiration is 180 days. This can either be customized or set to the expiration from the
Access Token.

=== "Customize node expiration"

    ```yaml hl_lines="5"
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
      expiry: 30d   # Use 0 to disable node expiration
    ```

=== "Use expiration from Access Token"

    Please keep in mind that the Access Token is typically a short-lived token that expires within a few minutes. You
    will have to configure token expiration in your identity provider to avoid frequent re-authentication.


    ```yaml hl_lines="5"
    oidc:
      issuer: "https://sso.example.com"
      client_id: "headscale"
      client_secret: "generated-secret"
      use_expiry_from_token: true
    ```

!!! tip "Expire a node and force re-authentication"

    A node can be expired immediately via:
    ```console
    headscale node expire -i <NODE_ID>
    ```

### Reference a user in the policy

You may refer to users in the Headscale policy via:

- Email address
- Username
- Provider identifier (this value is currently only available from the [API](api.md), database or directly from your
  identity provider)

!!! note "A user identifier in the policy must contain a single `@`"

    The Headscale policy requires a single `@` to reference a user. If the username or provider identifier doesn't
    already contain a single `@`, it needs to be appended at the end. For example: the username `ssmith` has to be
    written as `ssmith@` to be correctly identified as user within the policy.

!!! warning "Email address or username might be updated by users"

    Many identity providers allow users to update their own profile. Depending on the identity provider and its
    configuration, the values for username or email address might change over time. This might have unexpected
    consequences for Headscale where a policy might no longer work or a user might obtain more access by hijacking an
    existing username or email address.

!!! tip "Howto use the provider identifier in the policy"

    The provider identifier uniquely identifies an OIDC user and a well-behaving identity provider guarantees that this
    value never changes for a particular user. It is usually an opaque and long string and its value is currently only
    available from the [API](api.md), database or directly from your identity provider).

    Use the [API](api.md) with the `/api/v1/user` endpoint to fetch the provider identifier (`providerId`). The value
    (be sure to append an `@` in case the provider identifier doesn't already contain an `@` somewhere) can be used
    directly to reference a user in the policy. To improve readability of the policy, one may use the `groups` section
    as an alias:

    ```json
    {
      "groups": {
        "group:alice": [
          "https://soo.example.com/oauth2/openid/59ac9125-c31b-46c5-814e-06242908cf57@"
        ]
      },
      "acls": [
        {
          "action": "accept",
          "src": ["group:alice"],
          "dst": ["*:*"]
        }
      ]
    }
    ```

## Supported OIDC claims

Headscale uses [the standard OIDC claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) to
populate and update its local user profile on each login. OIDC claims are read from the ID Token and from the UserInfo
endpoint.

| Headscale profile   | OIDC claim           | Notes / examples                                                                                  |
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
| email address       | `email`              | Only verified emails are synchronized, unless `email_verified_required: false` is configured      |
| display name        | `name`               | eg: `Sam Smith`                                                                                   |
| 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)                           |

## 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 ACLs.
- 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 `@`.
    - The username must start with a letter.

Please see the [GitHub label "OIDC"](https://github.com/juanfont/headscale/labels/OIDC) for OIDC related issues.

## Identity provider specific configuration

!!! warning "Third-party software and services"

    This section of the documentation is specific for third-party software and services. We recommend users read the
    third-party documentation on how to configure and integrate an OIDC client. Please see the [Configuration
    section](#configuration) for a description of Headscale's OIDC related configuration settings.

Any identity provider with OpenID Connect support should "just work" with Headscale. The following identity providers
are known to work:

- [Authelia](#authelia)
- [Authentik](#authentik)
- [Kanidm](#kanidm)
- [Keycloak](#keycloak)

### Authelia

Authelia is fully supported by Headscale.

### Authentik

- Authentik is fully supported by Headscale.
- [Headscale does not support JSON Web Encryption](https://github.com/juanfont/headscale/issues/2446). Leave the field
  `Encryption Key` in the providers section unset.

### Google OAuth

!!! warning "No username due to missing preferred_username"

    Google OAuth does not send the `preferred_username` claim when the scope `profile` is requested. The username in
    Headscale will be blank/not set.

In order to integrate Headscale with Google, you'll need to have a [Google Cloud
Console](https://console.cloud.google.com) account.

Google OAuth has a [verification process](https://support.google.com/cloud/answer/9110914?hl=en) if you need to have
users authenticate who are outside of your domain. If you only need to authenticate users from your domain name (ie
`@example.com`), you don't need to go through the verification process.

However if you don't have a domain, or need to add users outside of your domain, you can manually add emails via Google
Console.

#### Steps

1. Go to [Google Console](https://console.cloud.google.com) and login or create an account if you don't have one.
2. Create a project (if you don't already have one).
3. On the left hand menu, go to `APIs and services` -> `Credentials`
4. Click `Create Credentials` -> `OAuth client ID`
5. Under `Application Type`, choose `Web Application`
6. For `Name`, enter whatever you like
7. Under `Authorised redirect URIs`, add Headscale's OIDC callback URL: `https://headscale.example.com/oidc/callback`
8. Click `Save` at the bottom of the form
9. Take note of the `Client ID` and `Client secret`, you can also download it for reference if you need it.
10. [Configure Headscale following the "Basic configuration" steps](#basic-configuration). The issuer URL for Google
    OAuth is: `https://accounts.google.com`.

### Kanidm

- Kanidm is fully supported by Headscale.
- Groups for the [allowed groups filter](#authorize-users-with-filters) need to be specified with their full SPN, for
  example: `headscale_users@sso.example.com`.
- Kanidm sends the full SPN (`alice@sso.example.com`) as `preferred_username` by default. Headscale stores this value as
  username which might be confusing as the username and email fields now contain values that look like an email address.
  [Kanidm can be configured to send the short username as `preferred_username` attribute
  instead](https://kanidm.github.io/kanidm/stable/integrations/oauth2.html#short-names):
  ```console
  kanidm system oauth2 prefer-short-username <client name>
  ```
  Once configured, the short username in Headscale will be `alice` and can be referred to as `alice@` in the policy.

### Keycloak

Keycloak is fully supported by Headscale.

#### Additional configuration to use the allowed groups filter

Keycloak has no built-in client scope for the OIDC `groups` claim. This extra configuration step is **only** needed if
you need to [authorize access based on group membership](#authorize-users-with-filters).

- Create a new client scope `groups` for OpenID Connect:
    - Configure a `Group Membership` mapper with name `groups` and the token claim name `groups`.
    - Add the mapper to at least the UserInfo endpoint.
- Configure the new client scope for your Headscale client:
    - Edit the Headscale client.
    - Search for the client scope `group`.
    - Add it with assigned type `Default`.
- [Configure the allowed groups in Headscale](#authorize-users-with-filters). How groups need to be specified depends on
  Keycloak's `Full group path` option:
    - `Full group path` is enabled: groups contain their full path, e.g. `/top/group1`
    - `Full group path` is disabled: only the name of the group is used, e.g. `group1`

### Microsoft Entra ID

In order to integrate Headscale with Microsoft Entra ID, you'll need to provision an App Registration with the correct
scopes and redirect URI.

[Configure Headscale following the "Basic configuration" steps](#basic-configuration). The issuer URL for Microsoft
Entra ID is: `https://login.microsoftonline.com/<tenant-UUID>/v2.0`. The following `extra_params` might be useful:

- `domain_hint: example.com` to use your own domain
- `prompt: select_account` to force an account picker during login

When using Microsoft Entra ID together with the [allowed groups filter](#authorize-users-with-filters), configure the
Headscale OIDC scope without the `groups` claim, for example:

```yaml
oidc:
  scope: ["openid", "profile", "email"]
```

Groups for the [allowed groups filter](#authorize-users-with-filters) need to be specified with their group ID(UUID) instead
of the group name.
