# How OIDC SSO Integration Works with Claims-Based Admin Mapping in TREK

> Learn how TREK's OIDC SSO integration uses claims-based admin mapping to automatically grant admin roles based on group claims, enhancing security and simplifying access management.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: deep-dive
- Published: 2026-06-26

---

**TREK determines administrator privileges during OIDC SSO authentication by inspecting a configurable claim (default `groups`) for a specific value defined in `OIDC_ADMIN_VALUE`, automatically granting the `admin` role when the claim value matches while protecting the bootstrap user and last remaining admin from demotion.**

TREK implements OpenID Connect (OIDC) single sign-on to delegate authentication to external identity providers like Google, Keycloak, or Authentik. After verifying the ID token, the system performs claims-based admin mapping to determine whether the authenticated user should receive administrator privileges or standard user access. This mapping relies on environment variables to inspect specific claims in the OIDC userinfo response, making it easy to synchronize your identity provider's group memberships with TREK's role-based access control.

## Configuration Environment Variables

Claims-based admin mapping is controlled exclusively through environment variables. These settings cannot be modified from the admin panel and must be configured before starting the application.

- **`OIDC_ADMIN_CLAIM`**: Specifies the claim name to inspect for role information (default: `groups`). Common alternatives include `roles` or `realm_access.roles`.
- **`OIDC_ADMIN_VALUE`**: The exact value that must be present in the claim to grant admin privileges. When unset, claim-based admin mapping is disabled and all users receive the standard `user` role.
- **`OIDC_ISSUER`**, **`OIDC_CLIENT_ID`**, **`OIDC_CLIENT_SECRET`**: Standard OIDC configuration for provider discovery and token exchange.

For a complete reference, see the environment variable documentation in [`wiki/OIDC-SSO.md`](https://github.com/mauriceboe/TREK/blob/main/wiki/OIDC-SSO.md) and [`wiki/Environment-Variables.md`](https://github.com/mauriceboe/TREK/blob/main/wiki/Environment-Variables.md).

## Authentication Flow Overview

The OIDC SSO process follows these steps to authenticate and authorize users:

1. The user initiates login via `GET /api/auth/oidc/login`, which redirects to the identity provider.
2. After authentication, the IdP redirects to `GET /api/auth/oidc/callback` with an authorization code.
3. TREK exchanges the code for tokens using `exchangeCodeForToken()` in [`server/src/services/oidcService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/oidcService.ts).
4. The ID token is verified via `verifyIdToken()` using the provider's JWKS.
5. User information is retrieved via `getUserInfo()`.
6. The system calls `findOrCreateUser()` to link the OIDC identity to a local account or create a new one.
7. **Role resolution** occurs through `resolveOidcRole()`, which determines if the user receives `admin` or `user` privileges.

## Claims-Based Admin Mapping Logic

The core role resolution logic resides in [`server/src/services/oidcService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/oidcService.ts) within the `resolveOidcRole` function. This function implements a hierarchy of checks to determine the appropriate role.

### First User Protection

The bootstrap administrator (the first account ever created in the TREK instance) is automatically granted admin privileges regardless of OIDC claims. This ensures you cannot lock yourself out during initial setup.

### Claim Inspection

For subsequent users, if `OIDC_ADMIN_VALUE` is configured, TREK examines the claim specified by `OIDC_ADMIN_CLAIM`:

```typescript
export function resolveOidcRole(userInfo: OidcUserInfo, isFirstUser: boolean): 'admin' | 'user' {
  // First user always becomes admin
  if (isFirstUser) return 'admin';

  const adminValue = process.env.OIDC_ADMIN_VALUE;
  if (!adminValue) return 'user'; // Claim-based admin disabled

  const claimKey = process.env.OIDC_ADMIN_CLAIM || 'groups';
  const claimData = userInfo[claimKey];

  // Handle array claims (e.g., ["admins", "users"])
  if (Array.isArray(claimData)) {
    return claimData.some(v => String(v) === adminValue) ? 'admin' : 'user';
  }
  
  // Handle string claims
  if (typeof claimData === 'string') {
    return claimData === adminValue ? 'admin' : 'user';
  }
  
  return 'user';
}

```

The function handles both array and string claim formats, converting values to strings for comparison against `OIDC_ADMIN_VALUE`.

### Role Re-evaluation and Safety

TREK stores the resolved role in the `users.role` column and re-evaluates it on every login. However, the system includes a critical safety check: if the resolved role would downgrade the **last remaining admin**, TREK logs a warning and preserves the admin role to prevent accidental lock-out.

## Configuration Examples

### Docker Compose Setup

Configure claims-based admin mapping in your [`docker-compose.yml`](https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml):

```yaml
services:
  trek:
    environment:
      - OIDC_ISSUER=https://auth.example.com
      - OIDC_CLIENT_ID=trek
      - OIDC_CLIENT_SECRET=super-secret
      - OIDC_ADMIN_CLAIM=groups          # Claim to inspect

      - OIDC_ADMIN_VALUE=app-trek-admins # Value granting admin access

```

### Custom Role Claims

For identity providers using `roles` instead of `groups`:

```bash
export OIDC_ADMIN_CLAIM=roles
export OIDC_ADMIN_VALUE=administrator

```

Users with `"roles": ["administrator", "editor"]` in their ID token will receive admin privileges.

### Programmatic Role Verification

When building custom middleware or extensions, you can reuse the role resolution logic:

```typescript
import { resolveOidcRole } from './services/oidcService';

function checkAdminAccess(userInfo: OidcUserInfo): boolean {
  // Pass false for isFirstUser when checking existing users
  return resolveOidcRole(userInfo, false) === 'admin';
}

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`server/src/services/oidcService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/oidcService.ts) | Core OIDC implementation including `resolveOidcRole()`, `findOrCreateUser()`, and token validation |
| [`wiki/OIDC-SSO.md`](https://github.com/mauriceboe/TREK/blob/main/wiki/OIDC-SSO.md) | High-level documentation of the OIDC flow and required environment variables |
| [`wiki/Environment-Variables.md`](https://github.com/mauriceboe/TREK/blob/main/wiki/Environment-Variables.md) | Complete reference for all OIDC-related configuration |
| [`server/tests/unit/services/oidcService.test.ts`](https://github.com/mauriceboe/TREK/blob/main/server/tests/unit/services/oidcService.test.ts) | Unit tests covering claims-based admin mapping scenarios |
| [`docker-compose.yml`](https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml) | Example production configuration showing OIDC variable placement |

## Summary

- **Claims-based admin mapping** uses `OIDC_ADMIN_CLAIM` (default: `groups`) and `OIDC_ADMIN_VALUE` to determine administrator privileges from OIDC tokens.
- The first user in the system is automatically granted admin status regardless of claims.
- The `resolveOidcRole()` function in [`server/src/services/oidcService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/oidcService.ts) handles both array and string claim formats.
- Roles are re-evaluated on every login, with safety mechanisms preventing the demotion of the last remaining admin.
- Configuration is environment-variable only and cannot be changed through the web interface.

## Frequently Asked Questions

### What happens if OIDC_ADMIN_VALUE is not set?

When `OIDC_ADMIN_VALUE` is undefined, claims-based admin mapping is disabled. Every user authenticating via SSO receives the standard `user` role, and no automatic admin promotion occurs based on OIDC claims. You must manually promote users to admin through the database or admin panel.

### Can I use nested claims for role mapping?

The current implementation in `resolveOidcRole()` performs a direct property lookup on the userinfo object using `userInfo[claimKey]`. Nested claims (e.g., `realm_access.roles`) require your identity provider to flatten the claim or you must configure a custom claim mapper in your IdP to surface the nested value as a top-level claim.

### Does TREK support multiple admin values or regex patterns?

As implemented in [`server/src/services/oidcService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/oidcService.ts), the comparison uses exact string equality (`===`). The code checks if any array element equals the `OIDC_ADMIN_VALUE` or if the string claim equals it exactly. Multiple values or regex patterns are not supported; you must specify a single exact value.

### How does TREK handle claim value type coercion?

The function explicitly converts array elements to strings using `String(v)` before comparison, ensuring consistent matching regardless of whether the IdP sends integers or strings. However, the `OIDC_ADMIN_VALUE` environment variable is always treated as a string for comparison purposes.