given the func (v *Validator) refreshKeys(ctx context.Context) {
    v.lock.Lock()
    defer v.lock.Unlock()

    refreshedKeys, err := getPemKeys(v.keysLocation)
    if err != nil {
        log.WithContext(ctx).Debugf("cannot get JSONWebKey: %v, falling back to old keys", err)
        return
    }

    log.WithContext(ctx).Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
    v.keys = refreshedKeys
} I want to have another function specifically made for the embeddedIdp. We can fetch the lkeys directly from dex provider. But we need to keep the original one as not everyone is using the embeddedIdp. Dex db has keys table.


Let me explore the relevant code to understand the current structure.


Agent

Explore dex keys and embedded IDP

Prompt:
Search the netbird codebase for:
1. How the embedded IDP / dex is set up - look for "embeddedIdp", "dex", "embedded" in Go files
2. The dex database schema or any references to a "keys" table in dex
3. How `getPemKeys` works and what it returns
4. The `KeysResponse` or similar structs used for JWT keys
5. Any existing dex key fetching code

Be thorough - check shared/auth/, management/, and any dex-related packages.


Perfect! Now I have comprehensive information. Let me create a final summary document with all the findings:

## Comprehensive NetBird Embedded IDP/Dex Search Results

I've completed a thorough search of the NetBird codebase for embedded IDP/Dex setup and key management. Here's what I found:

---

### 1. **Embedded IDP Setup & Configuration**

**Main Configuration Files:**
- `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go` - Core embedded IDP manager
- `/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/provider.go` - Dex provider implementation
- `/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go` - YAML configuration handling

**Key Configuration Structure (`EmbeddedIdPConfig`):**
- `Enabled` - Flag to enable/disable embedded IDP
- `Issuer` - OIDC issuer URL (e.g., "https://management.netbird.io/oauth2")
- `LocalAddress` - Management server's local listen address for JWT validation via localhost
- `Storage` - Storage configuration (sqlite3 or postgres)
  - `Type` - Storage backend ("sqlite3" or "postgres")
  - `Config` - Type-specific configuration (file path for sqlite3, DSN for postgres)
- `DashboardRedirectURIs` - OAuth2 redirect URIs for dashboard
- `CLIRedirectURIs` - OAuth2 redirect URIs for CLI
- `Owner` - Initial admin user configuration
- `SignKeyRefreshEnabled` - Enables automatic key rotation
- `LocalAuthDisabled` - Disables local email/password auth when external IDPs are configured

---

### 2. **Dex Database Schema & Storage**

**Dex Storage Tables (from dexidp/dex):**
Dex uses the `storage` interface which supports:
- `passwords` table - User credentials (email, username, user_id, hash)
- `connectors` table - Identity provider configurations
- `clients` table - OAuth2 clients
- `keys` table - Signing keys (auto-managed by Dex server)
- Various other tables for auth codes, refresh tokens, etc.

**Storage Initialization:**
- Located in `/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/provider.go`
- Dex automatically manages the `keys` table internally (not directly accessed from NetBird code)
- Migration files are in `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/migration/`

**NetBird's setup_keys table** (in `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/testdata/store.sql`):
```sql
CREATE TABLE `setup_keys` (
  `id` text,
  `account_id` text,
  `key` text,
  `name` text,
  `type` text,
  `created_at` datetime,
  `expires_at` datetime,
  `updated_at` datetime,
  `revoked` numeric,
  `used_times` integer,
  `last_used` datetime,
  `auto_groups` text,
  `usage_limit` integer,
  `ephemeral` numeric,
  PRIMARY KEY (`id`)
);
```
(Note: This is different from Dex's signing keys - this is NetBird's setup keys for peer enrollment)

---

### 3. **getPemKeys & Key Fetching Implementation**

**Location:** `/Users/misha/Documents/GolandProjects/netbird/netbird/shared/auth/jwt/validator.go`

**`getPemKeys` Function Signature:**
```go
func getPemKeys(keysLocation string) (*Jwks, error)
```

**What it does:**
1. Takes a `keysLocation` URL (JWKS endpoint)
2. Makes an HTTP GET request to fetch the JWKS
3. Decodes the JSON response into a `Jwks` struct
4. Reads `Cache-Control` header to determine cache expiration
5. Returns the parsed keys with expiration time

**Cache Management:**
- Keys have an expiration time calculated from `Cache-Control: max-age`
- `stillValid()` checks if cached keys are still valid (with 5-second buffer)
- `refreshKeys()` method refreshes keys when expired
- `forceRefreshKeys()` has a 30-second cooldown to prevent abuse

---

### 4. **KeysResponse & JWKS Structures**

**`Jwks` Struct** (represents JWKS response):
```go
type Jwks struct {
    Keys          []JSONWebKey `json:"keys"`
    expiresInTime time.Time
}
```

**`JSONWebKey` Struct** (individual key in the JWKS):
```go
type JSONWebKey struct {
    Kty   string   `json:"kty"`     // Key type (RSA, EC)
    Kid   string   `json:"kid"`     // Key ID
    Use   string   `json:"use"`     // Key usage
    N     string   `json:"n"`       // RSA modulus (base64url)
    E     string   `json:"e"`       // RSA exponent (base64url)
    Crv   string   `json:"crv"`     // Elliptic curve (P-256, P-384, P-521)
    X     string   `json:"x"`       // EC X coordinate (base64url)
    Y     string   `json:"y"`       // EC Y coordinate (base64url)
    X5c   []string `json:"x5c"`     // Certificate chain
}
```

**Key Endpoints:**
- External keys: `{issuer}/keys` (e.g., "https://management.netbird.io/oauth2/keys")
- Local keys: `http://localhost:{port}/oauth2/keys` (for internal validation)
- Discovery: `{issuer}/.well-known/openid-configuration`

---

### 5. **Existing Dex Key Fetching Code**

**Integration Point 1 - JWT Validator:**
- `/Users/misha/Documents/GolandProjects/netbird/netbird/shared/auth/jwt/validator.go`
- `NewValidator()` calls `getPemKeys(keysLocation)` at initialization
- `getKeyFunc()` uses cached keys to validate JWT tokens
- Supports key refresh on demand if `idpSignkeyRefreshEnabled` is true

**Integration Point 2 - Auth Manager Setup:**
- `/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go`
- Lines 82-86: Uses `oauthProvider.GetLocalKeysLocation()` for embedded Dex
- This provides the localhost JWKS endpoint for internal validation
- Automatically enables key refresh (`signingKeyRefreshEnabled = true`)

**Integration Point 3 - SSH Server:**
- `/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/engine_ssh.go`
- Uses `protoJWT.GetKeysLocation()` to configure JWT validation

---

### 6. **Embedded Dex Connector Management**

**Location:** `/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/connector.go`

**Local Connector Management:**
- `HasNonLocalConnectors(ctx)` - Checks if external IDPs are configured
- `DisableLocalAuth(ctx)` - Removes the local password connector
- `EnableLocalAuth(ctx)` - Creates local password connector if it doesn't exist
- `ensureLocalConnector(ctx, stor)` - Ensures local connector exists during initialization

**Connector Operations:**
- `CreateConnector(ctx, cfg)` - Creates new external IDP connector
- `GetConnector(ctx, id)` - Retrieves connector by ID
- `ListConnectors(ctx)` - Lists all non-local connectors
- `UpdateConnector(ctx, cfg)` - Updates existing connector
- `DeleteConnector(ctx, id)` - Deletes connector (prevents deletion of local)

**Supported Connector Types:**
- OIDC-based: oidc, zitadel, entra, okta, pocketid, authentik, keycloak
- OAuth2: google, microsoft
- Local: password authentication

---

### 7. **User ID Encoding/Decoding**

**Dex Composite User ID Format:**
Dex encodes user IDs as base64-encoded protobuf with two fields:
- Field 1: actual user ID (string)
- Field 2: connector ID (string, typically "local")

**Functions:**
- `EncodeDexUserID(userID, connectorID string)` - Encodes in provider.go
- `DecodeDexUserID(encodedID string)` - Decodes in provider.go
- `encodeDexUserID()` - Alternative encoding in dex.go
- `parseDexUserID(compositeID string)` - Parses to extract raw user ID in dex.go

---

### 8. **Keys Endpoint Implementations**

**Dex Provider Key Endpoints:**

```go
// GetKeysLocation() returns external JWKS URL
func (p *Provider) GetKeysLocation() string {
    return p.GetIssuer() + "/keys"
}

// GetIssuer() returns normalized issuer
func (p *Provider) GetIssuer() string {
    // Returns issuer with /oauth2 suffix
}

// GetTokenEndpoint() returns token endpoint
func (p *Provider) GetTokenEndpoint() string {
    return p.GetIssuer() + "/token"
}

// GetAuthorizationEndpoint() returns auth endpoint
func (p *Provider) GetAuthorizationEndpoint() string {
    return p.GetIssuer() + "/auth"
}

// GetDeviceAuthEndpoint() returns device code endpoint
func (p *Provider) GetDeviceAuthEndpoint() string {
    return p.GetIssuer() + "/device/code"
}
```

**Embedded IDP Manager Methods:**
```go
// GetKeysLocation() - external URL
func (m *EmbeddedIdPManager) GetKeysLocation() string {
    return m.provider.GetKeysLocation()
}

// GetLocalKeysLocation() - localhost URL for internal use
func (m *EmbeddedIdPManager) GetLocalKeysLocation() string {
    // Constructs http://localhost:{port}/oauth2/keys
}
```

---

### 9. **Key Rotation & Refresh**

**Configuration:**
- `SignKeyRefreshEnabled` in EmbeddedIdPConfig
- Dex default: `RotateKeysAfter: 6 * time.Hour` (in Config struct)
- Dex also respects config: `Expiry.SigningKeys` YAML setting

**Automatic Refresh:**
- Dex automatically rotates keys internally
- NetBird's JWT validator fetches latest keys from JWKS endpoint
- Refresh happens when cache expires or on-demand if key not found

---

### 10. **Test Files**

- `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go` - Embedded IDP manager tests
- `/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/dex_test.go` - Dex manager tests
- `/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/provider_test.go` - Provider tests

---

### 11. **Configuration Files**

**YAML Config Format** (`dex.YAMLConfig`):
```yaml
issuer: https://management.netbird.io/oauth2
storage:
  type: sqlite3
  config:
    file: /path/to/dex.db
web:
  http: :5556
  https: ""
  allowedOrigins:
    - "*"
  allowedHeaders:
    - Authorization
    - Content-Type
oauth2:
  skipApprovalScreen: true
expiry:
  signingKeys: 6h
  idTokens: 24h
frontend:
  issuer: NetBird
  theme: light
enablePasswordDB: true
staticPasswords:
  - email: admin@example.com
    hash: $2a$10$... # bcrypt hash
    username: Admin
    userID: uuid-here
staticClients:
  - id: netbird-dashboard
    name: NetBird Dashboard
    public: true
    redirectURIs: [...]
  - id: netbird-cli
    name: NetBird CLI
    public: true
    redirectURIs: [...]
```

---

### 12. **Integration Points**

1. **Management Server Initialization**
   - Creates `EmbeddedIdPManager` if embedded IDP is enabled
   - Registers HTTP handler for `/oauth2/` path
   - Initializes with static clients (dashboard, CLI)

2. **Auth Manager**
   - Uses `oauthProvider.GetLocalKeysLocation()` for token validation
   - Enables key refresh by default when embedded IDP is used

3. **HTTP Handler**
   - Mounts Dex at `/oauth2/` path prefix
   - Dex expects full path (includes `/oauth2`)

---

### Summary

The NetBird codebase has a complete embedded Dex OIDC implementation with:
- Configurable storage (SQLite/PostgreSQL)
- Automatic key rotation management
- Support for external identity providers
- Local email/password authentication with disabling capability
- Comprehensive JWT key fetching and caching
- User ID encoding in Dex's protobuf format
- Device flow support for CLI authentication

All key management is delegated to Dex's internal mechanisms, with NetBird consuming keys via the standard JWKS endpoint with proper caching and refresh strategies.

Prompt:
Search the netbird codebase for:
1. How the embedded IDP / dex is set up - look for "embeddedIdp", "dex", "embedded" in Go files
2. The dex database schema or any references to a "keys" table in dex
3. How `getPemKeys` works and what it returns
4. The `KeysResponse` or similar structs used for JWT keys
5. Any existing dex key fetching code

Be thorough - check shared/auth/, management/, and any dex-related packages.


Let me look at the Dex provider and how the storage works to understand the key format.


1	// Package dex provides an embedded Dex OIDC identity provider.
2	package dex
3
4	import (
5		"context"
6		"encoding/base64"
7		"errors"
8		"fmt"
9		"log/slog"
10		"net"
11		"net/http"
12		"os"
13		"path/filepath"
14		"strings"
15		"sync"
16		"time"
17
18		dexapi "github.com/dexidp/dex/api/v2"
19		"github.com/dexidp/dex/server"
20		"github.com/dexidp/dex/storage"
21		"github.com/dexidp/dex/storage/sql"
22		"github.com/google/uuid"
23		"github.com/prometheus/client_golang/prometheus"
24		"golang.org/x/crypto/bcrypt"
25		"google.golang.org/grpc"
26	)
27
28	// Config matches what management/internals/server/server.go expects
29	type Config struct {
30		Issuer  string
31		Port    int
32		DataDir string
33		DevMode bool
34
35		// GRPCAddr is the address for the gRPC API (e.g., ":5557"). Empty disables gRPC.
36		GRPCAddr string
37	}
38
39	// Provider wraps a Dex server
40	type Provider struct {
41		config       *Config
42		yamlConfig   *YAMLConfig
43		dexServer    *server.Server
44		httpServer   *http.Server
45		listener     net.Listener
46		grpcServer   *grpc.Server
47		grpcListener net.Listener
48		storage      storage.Storage
49		logger       *slog.Logger
50		mu           sync.Mutex
51		running      bool
52	}
53
54	// NewProvider creates and initializes the Dex server
55	func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
56		if config.Issuer == "" {
57			return nil, fmt.Errorf("issuer is required")
58		}
59		if config.Port <= 0 {
60			return nil, fmt.Errorf("invalid port")
61		}
62		if config.DataDir == "" {
63			return nil, fmt.Errorf("data directory is required")
64		}
65
66		logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
67
68		// Ensure data directory exists
69		if err := os.MkdirAll(config.DataDir, 0700); err != nil {
70			return nil, fmt.Errorf("failed to create data directory: %w", err)
71		}
72
73		// Initialize SQLite storage
74		dbPath := filepath.Join(config.DataDir, "oidc.db")
75		sqliteConfig := &sql.SQLite3{File: dbPath}
76		stor, err := sqliteConfig.Open(logger)
77		if err != nil {
78			return nil, fmt.Errorf("failed to open storage: %w", err)
79		}
80
81		// Ensure a local connector exists (for password authentication)
82		if err := ensureLocalConnector(ctx, stor); err != nil {
83			stor.Close()
84			return nil, fmt.Errorf("failed to ensure local connector: %w", err)
85		}
86
87		// Ensure issuer ends with /oauth2 for proper path mounting
88		issuer := strings.TrimSuffix(config.Issuer, "/")
89		if !strings.HasSuffix(issuer, "/oauth2") {
90			issuer += "/oauth2"
91		}
92
93		// Build refresh token policy (required to avoid nil pointer panics)
94		refreshPolicy, err := server.NewRefreshTokenPolicy(logger, false, "", "", "")
95		if err != nil {
96			stor.Close()
97			return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
98		}
99
100		// Build Dex server config - use Dex's types directly
101		dexConfig := server.Config{
102			Issuer:                     issuer,
103			Storage:                    stor,
104			SkipApprovalScreen:         true,
105			SupportedResponseTypes:     []string{"code"},
106			ContinueOnConnectorFailure: true,
107			Logger:                     logger,
108			PrometheusRegistry:         prometheus.NewRegistry(),
109			RotateKeysAfter:            6 * time.Hour,
110			IDTokensValidFor:           24 * time.Hour,
111			RefreshTokenPolicy:         refreshPolicy,
112			Web: server.WebConfig{
113				Issuer: "NetBird",
114			},
115		}
116
117		dexSrv, err := server.NewServer(ctx, dexConfig)
118		if err != nil {
119			stor.Close()
120			return nil, fmt.Errorf("failed to create dex server: %w", err)
121		}
122
123		return &Provider{
124			config:    config,
125			dexServer: dexSrv,
126			storage:   stor,
127			logger:    logger,
128		}, nil
129	}
130
131	// NewProviderFromYAML creates and initializes the Dex server from a YAMLConfig
132	func NewProviderFromYAML(ctx context.Context, yamlConfig *YAMLConfig) (*Provider, error) {
133		// Configure log level from config, default to WARN to avoid logging sensitive data (emails)
134		logLevel := slog.LevelWarn
135		if yamlConfig.Logger.Level != "" {
136			switch strings.ToLower(yamlConfig.Logger.Level) {
137			case "debug":
138				logLevel = slog.LevelDebug
139			case "info":
140				logLevel = slog.LevelInfo
141			case "warn", "warning":
142				logLevel = slog.LevelWarn
143			case "error":
144				logLevel = slog.LevelError
145			}
146		}
147		logger := slog.New(NewLogrusHandler(logLevel))
148
149		stor, err := yamlConfig.Storage.OpenStorage(logger)
150		if err != nil {
151			return nil, fmt.Errorf("failed to open storage: %w", err)
152		}
153
154		if err := initializeStorage(ctx, stor, yamlConfig); err != nil {
155			stor.Close()
156			return nil, err
157		}
158
159		dexConfig := buildDexConfig(yamlConfig, stor, logger)
160		dexConfig.RefreshTokenPolicy, err = yamlConfig.GetRefreshTokenPolicy(logger)
161		if err != nil {
162			stor.Close()
163			return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
164		}
165
166		dexSrv, err := server.NewServer(ctx, dexConfig)
167		if err != nil {
168			stor.Close()
169			return nil, fmt.Errorf("failed to create dex server: %w", err)
170		}
171
172		return &Provider{
173			config:     &Config{Issuer: yamlConfig.Issuer, GRPCAddr: yamlConfig.GRPC.Addr},
174			yamlConfig: yamlConfig,
175			dexServer:  dexSrv,
176			storage:    stor,
177			logger:     logger,
178		}, nil
179	}
180
181	// initializeStorage sets up connectors, passwords, and clients in storage
182	func initializeStorage(ctx context.Context, stor storage.Storage, cfg *YAMLConfig) error {
183		if cfg.EnablePasswordDB {
184			if err := ensureLocalConnector(ctx, stor); err != nil {
185				return fmt.Errorf("failed to ensure local connector: %w", err)
186			}
187		}
188		if err := ensureStaticPasswords(ctx, stor, cfg.StaticPasswords); err != nil {
189			return err
190		}
191		if err := ensureStaticClients(ctx, stor, cfg.StaticClients); err != nil {
192			return err
193		}
194		return ensureStaticConnectors(ctx, stor, cfg.StaticConnectors)
195	}
196
197	// ensureStaticPasswords creates or updates static passwords in storage
198	func ensureStaticPasswords(ctx context.Context, stor storage.Storage, passwords []Password) error {
199		for _, pw := range passwords {
200			existing, err := stor.GetPassword(ctx, pw.Email)
201			if errors.Is(err, storage.ErrNotFound) {
202				if err := stor.CreatePassword(ctx, storage.Password(pw)); err != nil {
203					return fmt.Errorf("failed to create password for %s: %w", pw.Email, err)
204				}
205				continue
206			}
207			if err != nil {
208				return fmt.Errorf("failed to get password for %s: %w", pw.Email, err)
209			}
210			if string(existing.Hash) != string(pw.Hash) {
211				if err := stor.UpdatePassword(ctx, pw.Email, func(old storage.Password) (storage.Password, error) {
212					old.Hash = pw.Hash
213					old.Username = pw.Username
214					return old, nil
215				}); err != nil {
216					return fmt.Errorf("failed to update password for %s: %w", pw.Email, err)
217				}
218			}
219		}
220		return nil
221	}
222
223	// ensureStaticClients creates or updates static clients in storage
224	func ensureStaticClients(ctx context.Context, stor storage.Storage, clients []storage.Client) error {
225		for _, client := range clients {
226			_, err := stor.GetClient(ctx, client.ID)
227			if errors.Is(err, storage.ErrNotFound) {
228				if err := stor.CreateClient(ctx, client); err != nil {
229					return fmt.Errorf("failed to create client %s: %w", client.ID, err)
230				}
231				continue
232			}
233			if err != nil {
234				return fmt.Errorf("failed to get client %s: %w", client.ID, err)
235			}
236			if err := stor.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
237				old.RedirectURIs = client.RedirectURIs
238				old.Name = client.Name
239				old.Public = client.Public
240				return old, nil
241			}); err != nil {
242				return fmt.Errorf("failed to update client %s: %w", client.ID, err)
243			}
244		}
245		return nil
246	}
247
248	// buildDexConfig creates a server.Config with defaults applied
249	func buildDexConfig(yamlConfig *YAMLConfig, stor storage.Storage, logger *slog.Logger) server.Config {
250		cfg := yamlConfig.ToServerConfig(stor, logger)
251		cfg.PrometheusRegistry = prometheus.NewRegistry()
252		if cfg.RotateKeysAfter == 0 {
253			cfg.RotateKeysAfter = 24 * 30 * time.Hour
254		}
255		if cfg.IDTokensValidFor == 0 {
256			cfg.IDTokensValidFor = 24 * time.Hour
257		}
258		if cfg.Web.Issuer == "" {
259			cfg.Web.Issuer = "NetBird"
260		}
261		if len(cfg.SupportedResponseTypes) == 0 {
262			cfg.SupportedResponseTypes = []string{"code"}
263		}
264		cfg.ContinueOnConnectorFailure = true
265		return cfg
266	}
267
268	// Start starts the HTTP server and optionally the gRPC API server
269	func (p *Provider) Start(_ context.Context) error {
270		p.mu.Lock()
271		defer p.mu.Unlock()
272
273		if p.running {
274			return fmt.Errorf("already running")
275		}
276
277		// Determine listen address from config
278		var addr string
279		if p.yamlConfig != nil {
280			addr = p.yamlConfig.Web.HTTP
281			if addr == "" {
282				addr = p.yamlConfig.Web.HTTPS
283			}
284		} else if p.config != nil && p.config.Port > 0 {
285			addr = fmt.Sprintf(":%d", p.config.Port)
286		}
287		if addr == "" {
288			return fmt.Errorf("no listen address configured")
289		}
290
291		listener, err := net.Listen("tcp", addr)
292		if err != nil {
293			return fmt.Errorf("failed to listen on %s: %w", addr, err)
294		}
295		p.listener = listener
296
297		// Mount Dex at /oauth2/ path for reverse proxy compatibility
298		// Don't strip the prefix - Dex's issuer includes /oauth2 so it expects the full path
299		mux := http.NewServeMux()
300		mux.Handle("/oauth2/", p.dexServer)
301
302		p.httpServer = &http.Server{Handler: mux}
303		p.running = true
304
305		go func() {
306			if err := p.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
307				p.logger.Error("http server error", "error", err)
308			}
309		}()
310
311		// Start gRPC API server if configured
312		if p.config.GRPCAddr != "" {
313			if err := p.startGRPCServer(); err != nil {
314				// Clean up HTTP server on failure
315				_ = p.httpServer.Close()
316				_ = p.listener.Close()
317				return fmt.Errorf("failed to start gRPC server: %w", err)
318			}
319		}
320
321		p.logger.Info("HTTP server started", "addr", addr)
322		return nil
323	}
324
325	// startGRPCServer starts the gRPC API server using Dex's built-in API
326	func (p *Provider) startGRPCServer() error {
327		grpcListener, err := net.Listen("tcp", p.config.GRPCAddr)
328		if err != nil {
329			return fmt.Errorf("failed to listen on %s: %w", p.config.GRPCAddr, err)
330		}
331		p.grpcListener = grpcListener
332
333		p.grpcServer = grpc.NewServer()
334		// Use Dex's built-in API server implementation
335		// server.NewAPI(storage, logger, version, dexServer)
336		dexapi.RegisterDexServer(p.grpcServer, server.NewAPI(p.storage, p.logger, "netbird-dex", p.dexServer))
337
338		go func() {
339			if err := p.grpcServer.Serve(grpcListener); err != nil {
340				p.logger.Error("grpc server error", "error", err)
341			}
342		}()
343
344		p.logger.Info("gRPC API server started", "addr", p.config.GRPCAddr)
345		return nil
346	}
347
348	// Stop gracefully shuts down
349	func (p *Provider) Stop(ctx context.Context) error {
350		p.mu.Lock()
351		defer p.mu.Unlock()
352
353		if !p.running {
354			return nil
355		}
356
357		var errs []error
358
359		// Stop gRPC server first
360		if p.grpcServer != nil {
361			p.grpcServer.GracefulStop()
362			p.grpcServer = nil
363		}
364		if p.grpcListener != nil {
365			p.grpcListener.Close()
366			p.grpcListener = nil
367		}
368
369		if p.httpServer != nil {
370			if err := p.httpServer.Shutdown(ctx); err != nil {
371				errs = append(errs, err)
372			}
373		}
374
375		// Explicitly close listener as fallback (Shutdown should do this, but be safe)
376		if p.listener != nil {
377			if err := p.listener.Close(); err != nil {
378				// Ignore "use of closed network connection" - expected after Shutdown
379				if !strings.Contains(err.Error(), "use of closed") {
380					errs = append(errs, err)
381				}
382			}
383			p.listener = nil
384		}
385
386		if p.storage != nil {
387			if err := p.storage.Close(); err != nil {
388				errs = append(errs, err)
389			}
390		}
391
392		p.httpServer = nil
393		p.running = false
394
395		if len(errs) > 0 {
396			return fmt.Errorf("shutdown errors: %v", errs)
397		}
398		return nil
399	}
400
401	// EnsureDefaultClients creates dashboard and CLI OAuth clients
402	// Uses Dex's storage.Client directly - no custom wrappers
403	func (p *Provider) EnsureDefaultClients(ctx context.Context, dashboardURIs, cliURIs []string) error {
404		clients := []storage.Client{
405			{
406				ID:           "netbird-dashboard",
407				Name:         "NetBird Dashboard",
408				RedirectURIs: dashboardURIs,
409				Public:       true,
410			},
411			{
412				ID:           "netbird-cli",
413				Name:         "NetBird CLI",
414				RedirectURIs: cliURIs,
415				Public:       true,
416			},
417		}
418
419		for _, client := range clients {
420			_, err := p.storage.GetClient(ctx, client.ID)
421			if err == storage.ErrNotFound {
422				if err := p.storage.CreateClient(ctx, client); err != nil {
423					return fmt.Errorf("failed to create client %s: %w", client.ID, err)
424				}
425				continue
426			}
427			if err != nil {
428				return fmt.Errorf("failed to get client %s: %w", client.ID, err)
429			}
430			// Update if exists
431			if err := p.storage.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
432				old.RedirectURIs = client.RedirectURIs
433				return old, nil
434			}); err != nil {
435				return fmt.Errorf("failed to update client %s: %w", client.ID, err)
436			}
437		}
438
439		p.logger.Info("default OIDC clients ensured")
440		return nil
441	}
442
443	// Storage returns the underlying Dex storage for direct access
444	// Users can use storage.Client, storage.Password, storage.Connector directly
445	func (p *Provider) Storage() storage.Storage {
446		return p.storage
447	}
448
449	// Handler returns the Dex server as an http.Handler for embedding in another server.
450	// The handler expects requests with path prefix "/oauth2/".
451	func (p *Provider) Handler() http.Handler {
452		return p.dexServer
453	}
454
455	// CreateUser creates a new user with the given email, username, and password.
456	// Returns the encoded user ID in Dex's format (base64-encoded protobuf with connector ID).
457	func (p *Provider) CreateUser(ctx context.Context, email, username, password string) (string, error) {
458		hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
459		if err != nil {
460			return "", fmt.Errorf("failed to hash password: %w", err)
461		}
462
463		userID := uuid.New().String()
464		err = p.storage.CreatePassword(ctx, storage.Password{
465			Email:    email,
466			Username: username,
467			UserID:   userID,
468			Hash:     hash,
469		})
470		if err != nil {
471			return "", err
472		}
473
474		// Encode the user ID in Dex's format: base64(protobuf{user_id, connector_id})
475		// This matches the format Dex uses in JWT tokens
476		encodedID := EncodeDexUserID(userID, "local")
477		return encodedID, nil
478	}
479
480	// EncodeDexUserID encodes user ID and connector ID into Dex's base64-encoded protobuf format.
481	// Dex uses this format for the 'sub' claim in JWT tokens.
482	// Format: base64(protobuf message with field 1 = user_id, field 2 = connector_id)
483	func EncodeDexUserID(userID, connectorID string) string {
484		// Manually encode protobuf: field 1 (user_id) and field 2 (connector_id)
485		// Wire type 2 (length-delimited) for strings
486		var buf []byte
487
488		// Field 1: user_id (tag = 0x0a = field 1, wire type 2)
489		buf = append(buf, 0x0a)
490		buf = append(buf, byte(len(userID)))
491		buf = append(buf, []byte(userID)...)
492
493		// Field 2: connector_id (tag = 0x12 = field 2, wire type 2)
494		buf = append(buf, 0x12)
495		buf = append(buf, byte(len(connectorID)))
496		buf = append(buf, []byte(connectorID)...)
497
498		return base64.RawStdEncoding.EncodeToString(buf)
499	}
500
501	// DecodeDexUserID decodes Dex's base64-encoded user ID back to the raw user ID and connector ID.
502	func DecodeDexUserID(encodedID string) (userID, connectorID string, err error) {
503		// Try RawStdEncoding first, then StdEncoding (with padding)
504		buf, err := base64.RawStdEncoding.DecodeString(encodedID)
505		if err != nil {
506			buf, err = base64.StdEncoding.DecodeString(encodedID)
507			if err != nil {
508				return "", "", fmt.Errorf("failed to decode base64: %w", err)
509			}
510		}
511
512		// Parse protobuf manually
513		i := 0
514		for i < len(buf) {
515			if i >= len(buf) {
516				break
517			}
518			tag := buf[i]
519			i++
520
521			fieldNum := tag >> 3
522			wireType := tag & 0x07
523
524			if wireType != 2 { // We only expect length-delimited strings
525				return "", "", fmt.Errorf("unexpected wire type %d", wireType)
526			}
527
528			if i >= len(buf) {
529				return "", "", fmt.Errorf("truncated message")
530			}
531			length := int(buf[i])
532			i++
533
534			if i+length > len(buf) {
535				return "", "", fmt.Errorf("truncated string field")
536			}
537			value := string(buf[i : i+length])
538			i += length
539
540			switch fieldNum {
541			case 1:
542				userID = value
543			case 2:
544				connectorID = value
545			}
546		}
547
548		return userID, connectorID, nil
549	}
550
551	// GetUser returns a user by email
552	func (p *Provider) GetUser(ctx context.Context, email string) (storage.Password, error) {
553		return p.storage.GetPassword(ctx, email)
554	}
555
556	// GetUserByID returns a user by user ID.
557	// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
558	// Note: This requires iterating through all users since dex storage doesn't index by userID.
559	func (p *Provider) GetUserByID(ctx context.Context, userID string) (storage.Password, error) {
560		// Try to decode the user ID in case it's encoded
561		rawUserID, _, err := DecodeDexUserID(userID)
562		if err != nil {
563			// If decoding fails, assume it's already a raw UUID
564			rawUserID = userID
565		}
566
567		users, err := p.storage.ListPasswords(ctx)
568		if err != nil {
569			return storage.Password{}, fmt.Errorf("failed to list users: %w", err)
570		}
571		for _, user := range users {
572			if user.UserID == rawUserID {
573				return user, nil
574			}
575		}
576		return storage.Password{}, storage.ErrNotFound
577	}
578
579	// DeleteUser removes a user by email
580	func (p *Provider) DeleteUser(ctx context.Context, email string) error {
581		return p.storage.DeletePassword(ctx, email)
582	}
583
584	// ListUsers returns all users
585	func (p *Provider) ListUsers(ctx context.Context) ([]storage.Password, error) {
586		return p.storage.ListPasswords(ctx)
587	}
588
589	// UpdateUserPassword updates the password for a user identified by userID.
590	// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
591	// It verifies the current password before updating.
592	func (p *Provider) UpdateUserPassword(ctx context.Context, userID string, oldPassword, newPassword string) error {
593		// Get the user by ID to find their email
594		user, err := p.GetUserByID(ctx, userID)
595		if err != nil {
596			return fmt.Errorf("failed to get user: %w", err)
597		}
598
599		// Verify old password
600		if err := bcrypt.CompareHashAndPassword(user.Hash, []byte(oldPassword)); err != nil {
601			return fmt.Errorf("current password is incorrect")
602		}
603
604		// Hash the new password
605		newHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
606		if err != nil {
607			return fmt.Errorf("failed to hash new password: %w", err)
608		}
609
610		// Update the password in storage
611		err = p.storage.UpdatePassword(ctx, user.Email, func(old storage.Password) (storage.Password, error) {
612			old.Hash = newHash
613			return old, nil
614		})
615		if err != nil {
616			return fmt.Errorf("failed to update password: %w", err)
617		}
618
619		return nil
620	}
621
622	// GetIssuer returns the OIDC issuer URL.
623	func (p *Provider) GetIssuer() string {
624		if p.config == nil {
625			return ""
626		}
627		issuer := strings.TrimSuffix(p.config.Issuer, "/")
628		if !strings.HasSuffix(issuer, "/oauth2") {
629			issuer += "/oauth2"
630		}
631		return issuer
632	}
633
634	// GetKeysLocation returns the JWKS endpoint URL for token validation.
635	func (p *Provider) GetKeysLocation() string {
636		issuer := p.GetIssuer()
637		if issuer == "" {
638			return ""
639		}
640		return issuer + "/keys"
641	}
642
643	// GetTokenEndpoint returns the OAuth2 token endpoint URL.
644	func (p *Provider) GetTokenEndpoint() string {
645		issuer := p.GetIssuer()
646		if issuer == "" {
647			return ""
648		}
649		return issuer + "/token"
650	}
651
652	// GetDeviceAuthEndpoint returns the OAuth2 device authorization endpoint URL.
653	func (p *Provider) GetDeviceAuthEndpoint() string {
654		issuer := p.GetIssuer()
655		if issuer == "" {
656			return ""
657		}
658		return issuer + "/device/code"
659	}
660
661	// GetAuthorizationEndpoint returns the OAuth2 authorization endpoint URL.
662	func (p *Provider) GetAuthorizationEndpoint() string {
663		issuer := p.GetIssuer()
664		if issuer == "" {
665			return ""
666		}
667		return issuer + "/auth"
668	}
669

// Package dex provides an embedded Dex OIDC identity provider.
package dex

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	dexapi "github.com/dexidp/dex/api/v2"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/storage/sql"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc"
)

// Config matches what management/internals/server/server.go expects
type Config struct {
	Issuer  string
	Port    int
	DataDir string
	DevMode bool

	// GRPCAddr is the address for the gRPC API (e.g., ":5557"). Empty disables gRPC.
	GRPCAddr string
}

// Provider wraps a Dex server
type Provider struct {
	config       *Config
	yamlConfig   *YAMLConfig
	dexServer    *server.Server
	httpServer   *http.Server
	listener     net.Listener
	grpcServer   *grpc.Server
	grpcListener net.Listener
	storage      storage.Storage
	logger       *slog.Logger
	mu           sync.Mutex
	running      bool
}

// NewProvider creates and initializes the Dex server
func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
	if config.Issuer == "" {
		return nil, fmt.Errorf("issuer is required")
	}
	if config.Port <= 0 {
		return nil, fmt.Errorf("invalid port")
	}
	if config.DataDir == "" {
		return nil, fmt.Errorf("data directory is required")
	}

	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))

	// Ensure data directory exists
	if err := os.MkdirAll(config.DataDir, 0700); err != nil {
		return nil, fmt.Errorf("failed to create data directory: %w", err)
	}

	// Initialize SQLite storage
	dbPath := filepath.Join(config.DataDir, "oidc.db")
	sqliteConfig := &sql.SQLite3{File: dbPath}
	stor, err := sqliteConfig.Open(logger)
	if err != nil {
		return nil, fmt.Errorf("failed to open storage: %w", err)
	}

	// Ensure a local connector exists (for password authentication)
	if err := ensureLocalConnector(ctx, stor); err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to ensure local connector: %w", err)
	}

	// Ensure issuer ends with /oauth2 for proper path mounting
	issuer := strings.TrimSuffix(config.Issuer, "/")
	if !strings.HasSuffix(issuer, "/oauth2") {
		issuer += "/oauth2"
	}

	// Build refresh token policy (required to avoid nil pointer panics)
	refreshPolicy, err := server.NewRefreshTokenPolicy(logger, false, "", "", "")
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
	}

	// Build Dex server config - use Dex's types directly
	dexConfig := server.Config{
		Issuer:                     issuer,
		Storage:                    stor,
		SkipApprovalScreen:         true,
		SupportedResponseTypes:     []string{"code"},
		ContinueOnConnectorFailure: true,
		Logger:                     logger,
		PrometheusRegistry:         prometheus.NewRegistry(),
		RotateKeysAfter:            6 * time.Hour,
		IDTokensValidFor:           24 * time.Hour,
		RefreshTokenPolicy:         refreshPolicy,
		Web: server.WebConfig{
			Issuer: "NetBird",
		},
	}

	dexSrv, err := server.NewServer(ctx, dexConfig)
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create dex server: %w", err)
	}

	return &Provider{
		config:    config,
		dexServer: dexSrv,
		storage:   stor,
		logger:    logger,
	}, nil
}

// NewProviderFromYAML creates and initializes the Dex server from a YAMLConfig
func NewProviderFromYAML(ctx context.Context, yamlConfig *YAMLConfig) (*Provider, error) {
	// Configure log level from config, default to WARN to avoid logging sensitive data (emails)
	logLevel := slog.LevelWarn
	if yamlConfig.Logger.Level != "" {
		switch strings.ToLower(yamlConfig.Logger.Level) {
		case "debug":
			logLevel = slog.LevelDebug
		case "info":
			logLevel = slog.LevelInfo
		case "warn", "warning":
			logLevel = slog.LevelWarn
		case "error":
			logLevel = slog.LevelError
		}
	}
	logger := slog.New(NewLogrusHandler(logLevel))

	stor, err := yamlConfig.Storage.OpenStorage(logger)
	if err != nil {
		return nil, fmt.Errorf("failed to open storage: %w", err)
	}

	if err := initializeStorage(ctx, stor, yamlConfig); err != nil {
		stor.Close()
		return nil, err
	}

	dexConfig := buildDexConfig(yamlConfig, stor, logger)
	dexConfig.RefreshTokenPolicy, err = yamlConfig.GetRefreshTokenPolicy(logger)
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
	}

	dexSrv, err := server.NewServer(ctx, dexConfig)
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create dex server: %w", err)
	}

	return &Provider{
		config:     &Config{Issuer: yamlConfig.Issuer, GRPCAddr: yamlConfig.GRPC.Addr},
		yamlConfig: yamlConfig,
		dexServer:  dexSrv,
		storage:    stor,
		logger:     logger,
	}, nil
}

// initializeStorage sets up connectors, passwords, and clients in storage
func initializeStorage(ctx context.Context, stor storage.Storage, cfg *YAMLConfig) error {
	if cfg.EnablePasswordDB {
		if err := ensureLocalConnector(ctx, stor); err != nil {
			return fmt.Errorf("failed to ensure local connector: %w", err)
		}
	}
	if err := ensureStaticPasswords(ctx, stor, cfg.StaticPasswords); err != nil {
		return err
	}
	if err := ensureStaticClients(ctx, stor, cfg.StaticClients); err != nil {
		return err
	}
	return ensureStaticConnectors(ctx, stor, cfg.StaticConnectors)
}

// ensureStaticPasswords creates or updates static passwords in storage
func ensureStaticPasswords(ctx context.Context, stor storage.Storage, passwords []Password) error {
	for _, pw := range passwords {
		existing, err := stor.GetPassword(ctx, pw.Email)
		if errors.Is(err, storage.ErrNotFound) {
			if err := stor.CreatePassword(ctx, storage.Password(pw)); err != nil {
				return fmt.Errorf("failed to create password for %s: %w", pw.Email, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get password for %s: %w", pw.Email, err)
		}
		if string(existing.Hash) != string(pw.Hash) {
			if err := stor.UpdatePassword(ctx, pw.Email, func(old storage.Password) (storage.Password, error) {
				old.Hash = pw.Hash
				old.Username = pw.Username
				return old, nil
			}); err != nil {
				return fmt.Errorf("failed to update password for %s: %w", pw.Email, err)
			}
		}
	}
	return nil
}

// ensureStaticClients creates or updates static clients in storage
func ensureStaticClients(ctx context.Context, stor storage.Storage, clients []storage.Client) error {
	for _, client := range clients {
		_, err := stor.GetClient(ctx, client.ID)
		if errors.Is(err, storage.ErrNotFound) {
			if err := stor.CreateClient(ctx, client); err != nil {
				return fmt.Errorf("failed to create client %s: %w", client.ID, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get client %s: %w", client.ID, err)
		}
		if err := stor.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
			old.RedirectURIs = client.RedirectURIs
			old.Name = client.Name
			old.Public = client.Public
			return old, nil
		}); err != nil {
			return fmt.Errorf("failed to update client %s: %w", client.ID, err)
		}
	}
	return nil
}

// buildDexConfig creates a server.Config with defaults applied
func buildDexConfig(yamlConfig *YAMLConfig, stor storage.Storage, logger *slog.Logger) server.Config {
	cfg := yamlConfig.ToServerConfig(stor, logger)
	cfg.PrometheusRegistry = prometheus.NewRegistry()
	if cfg.RotateKeysAfter == 0 {
		cfg.RotateKeysAfter = 24 * 30 * time.Hour
	}
	if cfg.IDTokensValidFor == 0 {
		cfg.IDTokensValidFor = 24 * time.Hour
	}
	if cfg.Web.Issuer == "" {
		cfg.Web.Issuer = "NetBird"
	}
	if len(cfg.SupportedResponseTypes) == 0 {
		cfg.SupportedResponseTypes = []string{"code"}
	}
	cfg.ContinueOnConnectorFailure = true
	return cfg
}

// Start starts the HTTP server and optionally the gRPC API server
func (p *Provider) Start(_ context.Context) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.running {
		return fmt.Errorf("already running")
	}

	// Determine listen address from config
	var addr string
	if p.yamlConfig != nil {
		addr = p.yamlConfig.Web.HTTP
		if addr == "" {
			addr = p.yamlConfig.Web.HTTPS
		}
	} else if p.config != nil && p.config.Port > 0 {
		addr = fmt.Sprintf(":%d", p.config.Port)
	}
	if addr == "" {
		return fmt.Errorf("no listen address configured")
	}

	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return fmt.Errorf("failed to listen on %s: %w", addr, err)
	}
	p.listener = listener

	// Mount Dex at /oauth2/ path for reverse proxy compatibility
	// Don't strip the prefix - Dex's issuer includes /oauth2 so it expects the full path
	mux := http.NewServeMux()
	mux.Handle("/oauth2/", p.dexServer)

	p.httpServer = &http.Server{Handler: mux}
	p.running = true

	go func() {
		if err := p.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
			p.logger.Error("http server error", "error", err)
		}
	}()

	// Start gRPC API server if configured
	if p.config.GRPCAddr != "" {
		if err := p.startGRPCServer(); err != nil {
			// Clean up HTTP server on failure
			_ = p.httpServer.Close()
			_ = p.listener.Close()
			return fmt.Errorf("failed to start gRPC server: %w", err)
		}
	}

	p.logger.Info("HTTP server started", "addr", addr)
	return nil
}

// startGRPCServer starts the gRPC API server using Dex's built-in API
func (p *Provider) startGRPCServer() error {
	grpcListener, err := net.Listen("tcp", p.config.GRPCAddr)
	if err != nil {
		return fmt.Errorf("failed to listen on %s: %w", p.config.GRPCAddr, err)
	}
	p.grpcListener = grpcListener

	p.grpcServer = grpc.NewServer()
	// Use Dex's built-in API server implementation
	// server.NewAPI(storage, logger, version, dexServer)
	dexapi.RegisterDexServer(p.grpcServer, server.NewAPI(p.storage, p.logger, "netbird-dex", p.dexServer))

	go func() {
		if err := p.grpcServer.Serve(grpcListener); err != nil {
			p.logger.Error("grpc server error", "error", err)
		}
	}()

	p.logger.Info("gRPC API server started", "addr", p.config.GRPCAddr)
	return nil
}

// Stop gracefully shuts down
func (p *Provider) Stop(ctx context.Context) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if !p.running {
		return nil
	}

	var errs []error

	// Stop gRPC server first
	if p.grpcServer != nil {
		p.grpcServer.GracefulStop()
		p.grpcServer = nil
	}
	if p.grpcListener != nil {
		p.grpcListener.Close()
		p.grpcListener = nil
	}

	if p.httpServer != nil {
		if err := p.httpServer.Shutdown(ctx); err != nil {
			errs = append(errs, err)
		}
	}

	// Explicitly close listener as fallback (Shutdown should do this, but be safe)
	if p.listener != nil {
		if err := p.listener.Close(); err != nil {
			// Ignore "use of closed network connection" - expected after Shutdown
			if !strings.Contains(err.Error(), "use of closed") {
				errs = append(errs, err)
			}
		}
		p.listener = nil
	}

	if p.storage != nil {
		if err := p.storage.Close(); err != nil {
			errs = append(errs, err)
		}
	}

	p.httpServer = nil
	p.running = false

	if len(errs) > 0 {
		return fmt.Errorf("shutdown errors: %v", errs)
	}
	return nil
}

// EnsureDefaultClients creates dashboard and CLI OAuth clients
// Uses Dex's storage.Client directly - no custom wrappers
func (p *Provider) EnsureDefaultClients(ctx context.Context, dashboardURIs, cliURIs []string) error {
	clients := []storage.Client{
		{
			ID:           "netbird-dashboard",
			Name:         "NetBird Dashboard",
			RedirectURIs: dashboardURIs,
			Public:       true,
		},
		{
			ID:           "netbird-cli",
			Name:         "NetBird CLI",
			RedirectURIs: cliURIs,
			Public:       true,
		},
	}

	for _, client := range clients {
		_, err := p.storage.GetClient(ctx, client.ID)
		if err == storage.ErrNotFound {
			if err := p.storage.CreateClient(ctx, client); err != nil {
				return fmt.Errorf("failed to create client %s: %w", client.ID, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get client %s: %w", client.ID, err)
		}
		// Update if exists
		if err := p.storage.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
			old.RedirectURIs = client.RedirectURIs
			return old, nil
		}); err != nil {
			return fmt.Errorf("failed to update client %s: %w", client.ID, err)
		}
	}

	p.logger.Info("default OIDC clients ensured")
	return nil
}

// Storage returns the underlying Dex storage for direct access
// Users can use storage.Client, storage.Password, storage.Connector directly
func (p *Provider) Storage() storage.Storage {
	return p.storage
}

// Handler returns the Dex server as an http.Handler for embedding in another server.
// The handler expects requests with path prefix "/oauth2/".
func (p *Provider) Handler() http.Handler {
	return p.dexServer
}

// CreateUser creates a new user with the given email, username, and password.
// Returns the encoded user ID in Dex's format (base64-encoded protobuf with connector ID).
func (p *Provider) CreateUser(ctx context.Context, email, username, password string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", fmt.Errorf("failed to hash password: %w", err)
	}

	userID := uuid.New().String()
	err = p.storage.CreatePassword(ctx, storage.Password{
		Email:    email,
		Username: username,
		UserID:   userID,
		Hash:     hash,
	})
	if err != nil {
		return "", err
	}

	// Encode the user ID in Dex's format: base64(protobuf{user_id, connector_id})
	// This matches the format Dex uses in JWT tokens
	encodedID := EncodeDexUserID(userID, "local")
	return encodedID, nil
}

// EncodeDexUserID encodes user ID and connector ID into Dex's base64-encoded protobuf format.
// Dex uses this format for the 'sub' claim in JWT tokens.
// Format: base64(protobuf message with field 1 = user_id, field 2 = connector_id)
func EncodeDexUserID(userID, connectorID string) string {
	// Manually encode protobuf: field 1 (user_id) and field 2 (connector_id)
	// Wire type 2 (length-delimited) for strings
	var buf []byte

	// Field 1: user_id (tag = 0x0a = field 1, wire type 2)
	buf = append(buf, 0x0a)
	buf = append(buf, byte(len(userID)))
	buf = append(buf, []byte(userID)...)

	// Field 2: connector_id (tag = 0x12 = field 2, wire type 2)
	buf = append(buf, 0x12)
	buf = append(buf, byte(len(connectorID)))
	buf = append(buf, []byte(connectorID)...)

	return base64.RawStdEncoding.EncodeToString(buf)
}

// DecodeDexUserID decodes Dex's base64-encoded user ID back to the raw user ID and connector ID.
func DecodeDexUserID(encodedID string) (userID, connectorID string, err error) {
	// Try RawStdEncoding first, then StdEncoding (with padding)
	buf, err := base64.RawStdEncoding.DecodeString(encodedID)
	if err != nil {
		buf, err = base64.StdEncoding.DecodeString(encodedID)
		if err != nil {
			return "", "", fmt.Errorf("failed to decode base64: %w", err)
		}
	}

	// Parse protobuf manually
	i := 0
	for i < len(buf) {
		if i >= len(buf) {
			break
		}
		tag := buf[i]
		i++

		fieldNum := tag >> 3
		wireType := tag & 0x07

		if wireType != 2 { // We only expect length-delimited strings
			return "", "", fmt.Errorf("unexpected wire type %d", wireType)
		}

		if i >= len(buf) {
			return "", "", fmt.Errorf("truncated message")
		}
		length := int(buf[i])
		i++

		if i+length > len(buf) {
			return "", "", fmt.Errorf("truncated string field")
		}
		value := string(buf[i : i+length])
		i += length

		switch fieldNum {
		case 1:
			userID = value
		case 2:
			connectorID = value
		}
	}

	return userID, connectorID, nil
}

// GetUser returns a user by email
func (p *Provider) GetUser(ctx context.Context, email string) (storage.Password, error) {
	return p.storage.GetPassword(ctx, email)
}

// GetUserByID returns a user by user ID.
// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
// Note: This requires iterating through all users since dex storage doesn't index by userID.
func (p *Provider) GetUserByID(ctx context.Context, userID string) (storage.Password, error) {
	// Try to decode the user ID in case it's encoded
	rawUserID, _, err := DecodeDexUserID(userID)
	if err != nil {
		// If decoding fails, assume it's already a raw UUID
		rawUserID = userID
	}

	users, err := p.storage.ListPasswords(ctx)
	if err != nil {
		return storage.Password{}, fmt.Errorf("failed to list users: %w", err)
	}
	for _, user := range users {
		if user.UserID == rawUserID {
			return user, nil
		}
	}
	return storage.Password{}, storage.ErrNotFound
}

// DeleteUser removes a user by email
func (p *Provider) DeleteUser(ctx context.Context, email string) error {
	return p.storage.DeletePassword(ctx, email)
}

// ListUsers returns all users
func (p *Provider) ListUsers(ctx context.Context) ([]storage.Password, error) {
	return p.storage.ListPasswords(ctx)
}

// UpdateUserPassword updates the password for a user identified by userID.
// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
// It verifies the current password before updating.
func (p *Provider) UpdateUserPassword(ctx context.Context, userID string, oldPassword, newPassword string) error {
	// Get the user by ID to find their email
	user, err := p.GetUserByID(ctx, userID)
	if err != nil {
		return fmt.Errorf("failed to get user: %w", err)
	}

	// Verify old password
	if err := bcrypt.CompareHashAndPassword(user.Hash, []byte(oldPassword)); err != nil {
		return fmt.Errorf("current password is incorrect")
	}

	// Hash the new password
	newHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash new password: %w", err)
	}

	// Update the password in storage
	err = p.storage.UpdatePassword(ctx, user.Email, func(old storage.Password) (storage.Password, error) {
		old.Hash = newHash
		return old, nil
	})
	if err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	return nil
}

// GetIssuer returns the OIDC issuer URL.
func (p *Provider) GetIssuer() string {
	if p.config == nil {
		return ""
	}
	issuer := strings.TrimSuffix(p.config.Issuer, "/")
	if !strings.HasSuffix(issuer, "/oauth2") {
		issuer += "/oauth2"
	}
	return issuer
}

// GetKeysLocation returns the JWKS endpoint URL for token validation.
func (p *Provider) GetKeysLocation() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/keys"
}

// GetTokenEndpoint returns the OAuth2 token endpoint URL.
func (p *Provider) GetTokenEndpoint() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/token"
}

// GetDeviceAuthEndpoint returns the OAuth2 device authorization endpoint URL.
func (p *Provider) GetDeviceAuthEndpoint() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/device/code"
}

// GetAuthorizationEndpoint returns the OAuth2 authorization endpoint URL.
func (p *Provider) GetAuthorizationEndpoint() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/auth"
}


/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-80-
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-81-// Expiry holds configuration for the validity period of components.
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-82-type Expiry struct {
idp/dex/config.go:83:	SigningKeys    string              `yaml:"signingKeys" json:"signingKeys"`
idp/dex/config.go-84-	IDTokens       string              `yaml:"idTokens" json:"idTokens"`
idp/dex/config.go-85-	AuthRequests   string              `yaml:"authRequests" json:"authRequests"`
idp/dex/config.go-86-	DeviceRequests string              `yaml:"deviceRequests" json:"deviceRequests"`
--
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-412-	}
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-413-
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-414-	// Apply expiry settings
idp/dex/config.go:415:	if c.Expiry.SigningKeys != "" {
idp/dex/config.go:416:		if d, err := parseDuration(c.Expiry.SigningKeys); err == nil {
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-417-			cfg.RotateKeysAfter = d
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-418-		}
/Users/misha/Documents/GolandProjects/netbird/netbird/idp/dex/config.go-419-	}
--
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_prod.go-3-package installer
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_prod.go-4-
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_prod.go-5-const (
client/internal/updater/installer/repourl_prod.go:6:	DefaultSigningKeysBaseURL = "https://publickeys.netbird.io/artifact-signatures"
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_prod.go-7-)
--
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_dev.go-3-package installer
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_dev.go-4-
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_dev.go-5-const (
client/internal/updater/installer/repourl_dev.go:6:	DefaultSigningKeysBaseURL = "http://192.168.0.10:9089/signrepo"
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/repourl_dev.go-7-)
--
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/installer_common.go-70-			return err
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/installer_common.go-71-		}
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/installer_common.go-72-
client/internal/updater/installer/installer_common.go:73:		artifactVerify, err := reposign.NewArtifactVerify(DefaultSigningKeysBaseURL)
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/installer_common.go-74-		if err != nil {
client/internal/updater/installer/installer_common.go-75-			log.Errorf("failed to create artifact verify: %v", err)
/Users/misha/Documents/GolandProjects/netbird/netbird/client/internal/updater/installer/installer_common.go-76-			return err


Let me check the Dex storage interface for the `GetKeys` method.


Bash

go doc github.com/dexidp/dex/storage Storage 2>/dev/null | head -40

Check dex storage interface


go doc github.com/dexidp/dex/storage Keys 2>/dev/null

Check dex Keys type


package storage // import "github.com/dexidp/dex/storage"

type Storage interface {
	Close() error

	// TODO(ericchiang): Let the storages set the IDs of these objects.
	CreateAuthRequest(ctx context.Context, a AuthRequest) error
	CreateClient(ctx context.Context, c Client) error
	CreateAuthCode(ctx context.Context, c AuthCode) error
	CreateRefresh(ctx context.Context, r RefreshToken) error
	CreatePassword(ctx context.Context, p Password) error
	CreateOfflineSessions(ctx context.Context, s OfflineSessions) error
	CreateConnector(ctx context.Context, c Connector) error
	CreateDeviceRequest(ctx context.Context, d DeviceRequest) error
	CreateDeviceToken(ctx context.Context, d DeviceToken) error

	// TODO(ericchiang): return (T, bool, error) so we can indicate not found
	// requests that way instead of using ErrNotFound.
	GetAuthRequest(ctx context.Context, id string) (AuthRequest, error)
	GetAuthCode(ctx context.Context, id string) (AuthCode, error)
	GetClient(ctx context.Context, id string) (Client, error)
	GetKeys(ctx context.Context) (Keys, error)
	GetRefresh(ctx context.Context, id string) (RefreshToken, error)
	GetPassword(ctx context.Context, email string) (Password, error)
	GetOfflineSessions(ctx context.Context, userID string, connID string) (OfflineSessions, error)
	GetConnector(ctx context.Context, id string) (Connector, error)
	GetDeviceRequest(ctx context.Context, userCode string) (DeviceRequest, error)
	GetDeviceToken(ctx context.Context, deviceCode string) (DeviceToken, error)

	ListClients(ctx context.Context) ([]Client, error)
	ListRefreshTokens(ctx context.Context) ([]RefreshToken, error)
	ListPasswords(ctx context.Context) ([]Password, error)
	ListConnectors(ctx context.Context) ([]Connector, error)

	// Delete methods MUST be atomic.
	DeleteAuthRequest(ctx context.Context, id string) error
	DeleteAuthCode(ctx context.Context, code string) error
	DeleteClient(ctx context.Context, id string) error
	DeleteRefresh(ctx context.Context, id string) error
	DeletePassword(ctx context.Context, email string) error


package storage // import "github.com/dexidp/dex/storage"

type Keys struct {
	// Key for creating and verifying signatures. These may be nil.
	SigningKey    *jose.JSONWebKey
	SigningKeyPub *jose.JSONWebKey

	// Old signing keys which have been rotated but can still be used to validate
	// existing signatures.
	VerificationKeys []VerificationKey

	// The next time the signing key will rotate.
	//
	// For caching purposes, implementations MUST NOT update keys before this time.
	NextRotation time.Time
}
    Keys hold encryption and signing keys.


go doc github.com/dexidp/dex/storage VerificationKey 2>/dev/null && echo "---" && go doc github.com/go-jose/go-jose/v4 JSONWebKey 2>/dev/null | head -30

Check VerificationKey and JSONWebKey types


package storage // import "github.com/dexidp/dex/storage"

type VerificationKey struct {
	PublicKey *jose.JSONWebKey `json:"publicKey"`
	Expiry    time.Time        `json:"expiry"`
}
    VerificationKey is a rotated signing key which can still be used to verify
    signatures.

---
package jose // import "github.com/go-jose/go-jose/v4"

type JSONWebKey struct {
	// Key is the Go in-memory representation of this key. It must have one
	// of these types:
	//  - ed25519.PublicKey
	//  - ed25519.PrivateKey
	//  - *ecdsa.PublicKey
	//  - *ecdsa.PrivateKey
	//  - *rsa.PublicKey
	//  - *rsa.PrivateKey
	//  - []byte (a symmetric key)
	//
	// When marshaling this JSONWebKey into JSON, the "kty" header parameter
	// will be automatically set based on the type of this field.
	Key interface{}
	// Key identifier, parsed from `kid` header.
	KeyID string
	// Key algorithm, parsed from `alg` header.
	Algorithm string
	// Key use, parsed from `use` header.
	Use string

	// X.509 certificate chain, parsed from `x5c` header.
	Certificates []*x509.Certificate
	// X.509 certificate URL, parsed from `x5u` header.
	CertificatesURL *url.URL
	// X.509 certificate thumbprint (SHA-1), parsed from `x5t` header.
	CertificateThumbprintSHA1 []byte
	// X.509 certificate thumbprint (SHA-256), parsed from `x5t#S256` header.


Let me check how the Validator is constructed in the embedded IDP path.


/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-34-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-35-
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-36-func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-37-	// @note if invalid/missing parameters are sent the validator will instantiate
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-38-	// but it will fail when validating and parsing the token
management/server/auth/manager.go:39:	jwtValidator := nbjwt.NewValidator(
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-40-		issuer,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-41-		allAudiences,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-42-		keysLocation,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-43-		idpRefreshKeys,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/auth/manager.go-44-	)
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-44-		},
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-45-	}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-46-
management/internals/modules/reverseproxy/domain/validator_test.go-47-	for name, test := range tests {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-48-		t.Run(name, func(t *testing.T) {
management/internals/modules/reverseproxy/domain/validator_test.go:49:			validator := domain.NewValidator(test.resolver)
management/internals/modules/reverseproxy/domain/validator_test.go-50-			actual := validator.IsValid(t.Context(), test.domain, test.accept)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-51-			if test.expect != actual {
management/internals/modules/reverseproxy/domain/validator_test.go-52-				t.Errorf("Incorrect return value:\nexpect: %v\nactual: %v", test.expect, actual)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-53-			}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator_test.go-54-		})
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-14-
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-15-type Validator struct {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-16-	Resolver resolver
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-17-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-18-
management/internals/modules/reverseproxy/domain/validator.go:19:// NewValidator initializes a validator with a specific DNS Resolver.
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-20-// If a Validator is used without specifying a Resolver, then it will
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-21-// use the net.DefaultResolver.
management/internals/modules/reverseproxy/domain/validator.go:22:func NewValidator(resolver resolver) *Validator {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-23-	return &Validator{
management/internals/modules/reverseproxy/domain/validator.go-24-		Resolver: resolver,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-25-	}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-26-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/modules/reverseproxy/domain/validator.go-27-


Now let me see how the auth manager is created for the embedded IDP case.


/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-187-// OAuthConfigProvider defines the interface for OAuth configuration needed by auth flows.
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-188-type OAuthConfigProvider interface {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-189-	GetIssuer() string
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-190-	// GetKeysLocation returns the public JWKS endpoint URL (uses external issuer URL)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-191-	GetKeysLocation() string
management/server/idp/embedded.go:192:	// GetLocalKeysLocation returns the localhost JWKS endpoint URL for internal use.
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-193-	// Management server has embedded Dex and can validate tokens via localhost,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-194-	// avoiding external network calls and DNS resolution issues during startup.
management/server/idp/embedded.go:195:	GetLocalKeysLocation() string
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-196-	GetClientIDs() []string
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-197-	GetUserIDClaim() string
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-198-	GetTokenEndpoint() string
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-199-	GetDeviceAuthEndpoint() string
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-200-	GetAuthorizationEndpoint() string
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-596-// GetKeysLocation returns the JWKS endpoint URL for token validation.
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-597-func (m *EmbeddedIdPManager) GetKeysLocation() string {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-598-	return m.provider.GetKeysLocation()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-599-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-600-
management/server/idp/embedded.go:601:// GetLocalKeysLocation returns the localhost JWKS endpoint URL for internal token validation.
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-602-// Uses the LocalAddress from config (management server's listen address) since embedded Dex
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-603-// is served by the management HTTP server, not a standalone Dex server.
management/server/idp/embedded.go:604:func (m *EmbeddedIdPManager) GetLocalKeysLocation() string {
management/server/idp/embedded.go-605-	addr := m.config.LocalAddress
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-606-	if addr == "" {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-607-		return ""
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-608-	}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded.go-609-	// Construct localhost URL from listen address
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-311-		require.Error(t, err)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-312-		assert.Contains(t, err.Error(), "new password must be different")
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-313-	})
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-314-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-315-
management/server/idp/embedded_test.go:316:func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) {
management/server/idp/embedded_test.go-317-	ctx := context.Background()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-318-
management/server/idp/embedded_test.go-319-	tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-320-	require.NoError(t, err)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-321-	defer os.RemoveAll(tmpDir)
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-363-
management/server/idp/embedded_test.go-364-			manager, err := NewEmbeddedIdPManager(ctx, config, nil)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-365-			require.NoError(t, err)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-366-			defer func() { _ = manager.Stop(ctx) }()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-367-
management/server/idp/embedded_test.go:368:			result := manager.GetLocalKeysLocation()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-369-			assert.Equal(t, tt.expected, result)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-370-		})
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-371-	}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-372-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/server/idp/embedded_test.go-373-
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-66-
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-67-func (s *BaseServer) AuthManager() auth.Manager {
management/internals/server/controllers.go-68-	audiences := s.Config.GetAuthAudiences()
management/internals/server/controllers.go-69-	audience := s.Config.HttpConfig.AuthAudience
management/internals/server/controllers.go-70-	keysLocation := s.Config.HttpConfig.AuthKeysLocation
management/internals/server/controllers.go:71:	signingKeyRefreshEnabled := s.Config.HttpConfig.IdpSignKeyRefreshEnabled
management/internals/server/controllers.go-72-	issuer := s.Config.HttpConfig.AuthIssuer
management/internals/server/controllers.go-73-	userIDClaim := s.Config.HttpConfig.AuthUserIDClaim
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-74-
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-75-	// Use embedded IdP configuration if available
management/internals/server/controllers.go-76-	if oauthProvider := s.OAuthConfigProvider(); oauthProvider != nil {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-77-		audiences = oauthProvider.GetClientIDs()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-78-		if len(audiences) > 0 {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-79-			audience = audiences[0] // Use the first client ID as the primary audience
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-80-		}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-81-		// Use localhost keys location for internal validation (management has embedded Dex)
management/internals/server/controllers.go:82:		keysLocation = oauthProvider.GetLocalKeysLocation()
management/internals/server/controllers.go:83:		signingKeyRefreshEnabled = true
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-84-		issuer = oauthProvider.GetIssuer()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-85-		userIDClaim = oauthProvider.GetUserIDClaim()
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-86-	}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-87-
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-88-	return Create(s, func() auth.Manager {
--
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-90-			issuer,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-91-			audience,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-92-			keysLocation,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-93-			userIDClaim,
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-94-			audiences,
management/internals/server/controllers.go:95:			signingKeyRefreshEnabled)
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-96-	})
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-97-}
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-98-
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-99-func (s *BaseServer) EphemeralManager() ephemeral.Manager {
/Users/misha/Documents/GolandProjects/netbird/netbird/management/internals/server/controllers.go-100-	return Create(s, func() ephemeral.Manager {


Now I have a clear picture. Here's the plan:

1. Add a `GetJWKS` method to the Dex `Provider` that reads keys directly from storage
2. Add a `KeyFetcher` func type to the `Validator` so the embedded IDP can bypass HTTP
3. Wire it up in the controllers

Let me implement this.


1	package server
2
3	import (
4		"context"
5
6		log "github.com/sirupsen/logrus"
7
8		"github.com/netbirdio/management-integrations/integrations"
9		"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
10		proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"
11
12		"github.com/netbirdio/netbird/management/internals/controllers/network_map"
13		nmapcontroller "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
14		"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
15		"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral"
16		"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
17		"github.com/netbirdio/netbird/management/internals/shared/grpc"
18		"github.com/netbirdio/netbird/management/server"
19		"github.com/netbirdio/netbird/management/server/auth"
20		"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
21		"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
22		"github.com/netbirdio/netbird/management/server/job"
23	)
24
25	func (s *BaseServer) PeersUpdateManager() network_map.PeersUpdateManager {
26		return Create(s, func() network_map.PeersUpdateManager {
27			return update_channel.NewPeersUpdateManager(s.Metrics())
28		})
29	}
30
31	func (s *BaseServer) JobManager() *job.Manager {
32		return Create(s, func() *job.Manager {
33			return job.NewJobManager(s.Metrics(), s.Store(), s.PeersManager())
34		})
35	}
36
37	func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
38		return Create(s, func() integrated_validator.IntegratedValidator {
39			integratedPeerValidator, err := integrations.NewIntegratedValidator(
40				context.Background(),
41				s.PeersManager(),
42				s.SettingsManager(),
43				s.EventStore())
44			if err != nil {
45				log.Errorf("failed to create integrated peer validator: %v", err)
46			}
47			return integratedPeerValidator
48		})
49	}
50
51	func (s *BaseServer) ProxyController() port_forwarding.Controller {
52		return Create(s, func() port_forwarding.Controller {
53			return integrations.NewController(s.Store())
54		})
55	}
56
57	func (s *BaseServer) SecretsManager() grpc.SecretsManager {
58		return Create(s, func() grpc.SecretsManager {
59			secretsManager, err := grpc.NewTimeBasedAuthSecretsManager(s.PeersUpdateManager(), s.Config.TURNConfig, s.Config.Relay, s.SettingsManager(), s.GroupsManager())
60			if err != nil {
61				log.Fatalf("failed to create secrets manager: %v", err)
62			}
63			return secretsManager
64		})
65	}
66
67	func (s *BaseServer) AuthManager() auth.Manager {
68		audiences := s.Config.GetAuthAudiences()
69		audience := s.Config.HttpConfig.AuthAudience
70		keysLocation := s.Config.HttpConfig.AuthKeysLocation
71		signingKeyRefreshEnabled := s.Config.HttpConfig.IdpSignKeyRefreshEnabled
72		issuer := s.Config.HttpConfig.AuthIssuer
73		userIDClaim := s.Config.HttpConfig.AuthUserIDClaim
74
75		// Use embedded IdP configuration if available
76		if oauthProvider := s.OAuthConfigProvider(); oauthProvider != nil {
77			audiences = oauthProvider.GetClientIDs()
78			if len(audiences) > 0 {
79				audience = audiences[0] // Use the first client ID as the primary audience
80			}
81			// Use localhost keys location for internal validation (management has embedded Dex)
82			keysLocation = oauthProvider.GetLocalKeysLocation()
83			signingKeyRefreshEnabled = true
84			issuer = oauthProvider.GetIssuer()
85			userIDClaim = oauthProvider.GetUserIDClaim()
86		}
87
88		return Create(s, func() auth.Manager {
89			return auth.NewManager(s.Store(),
90				issuer,
91				audience,
92				keysLocation,
93				userIDClaim,
94				audiences,
95				signingKeyRefreshEnabled)
96		})
97	}
98
99	func (s *BaseServer) EphemeralManager() ephemeral.Manager {
100		return Create(s, func() ephemeral.Manager {

package server

import (
	"context"

	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/management-integrations/integrations"
	"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
	proxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy/manager"

	"github.com/netbirdio/netbird/management/internals/controllers/network_map"
	nmapcontroller "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
	"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
	"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral"
	"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
	"github.com/netbirdio/netbird/management/internals/shared/grpc"
	"github.com/netbirdio/netbird/management/server"
	"github.com/netbirdio/netbird/management/server/auth"
	"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
	"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
	"github.com/netbirdio/netbird/management/server/job"
)

func (s *BaseServer) PeersUpdateManager() network_map.PeersUpdateManager {
	return Create(s, func() network_map.PeersUpdateManager {
		return update_channel.NewPeersUpdateManager(s.Metrics())
	})
}

func (s *BaseServer) JobManager() *job.Manager {
	return Create(s, func() *job.Manager {
		return job.NewJobManager(s.Metrics(), s.Store(), s.PeersManager())
	})
}

func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
	return Create(s, func() integrated_validator.IntegratedValidator {
		integratedPeerValidator, err := integrations.NewIntegratedValidator(
			context.Background(),
			s.PeersManager(),
			s.SettingsManager(),
			s.EventStore())
		if err != nil {
			log.Errorf("failed to create integrated peer validator: %v", err)
		}
		return integratedPeerValidator
	})
}

func (s *BaseServer) ProxyController() port_forwarding.Controller {
	return Create(s, func() port_forwarding.Controller {
		return integrations.NewController(s.Store())
	})
}

func (s *BaseServer) SecretsManager() grpc.SecretsManager {
	return Create(s, func() grpc.SecretsManager {
		secretsManager, err := grpc.NewTimeBasedAuthSecretsManager(s.PeersUpdateManager(), s.Config.TURNConfig, s.Config.Relay, s.SettingsManager(), s.GroupsManager())
		if err != nil {
			log.Fatalf("failed to create secrets manager: %v", err)
		}
		return secretsManager
	})
}

func (s *BaseServer) AuthManager() auth.Manager {
	audiences := s.Config.GetAuthAudiences()
	audience := s.Config.HttpConfig.AuthAudience
	keysLocation := s.Config.HttpConfig.AuthKeysLocation
	signingKeyRefreshEnabled := s.Config.HttpConfig.IdpSignKeyRefreshEnabled
	issuer := s.Config.HttpConfig.AuthIssuer
	userIDClaim := s.Config.HttpConfig.AuthUserIDClaim

	// Use embedded IdP configuration if available
	if oauthProvider := s.OAuthConfigProvider(); oauthProvider != nil {
		audiences = oauthProvider.GetClientIDs()
		if len(audiences) > 0 {
			audience = audiences[0] // Use the first client ID as the primary audience
		}
		// Use localhost keys location for internal validation (management has embedded Dex)
		keysLocation = oauthProvider.GetLocalKeysLocation()
		signingKeyRefreshEnabled = true
		issuer = oauthProvider.GetIssuer()
		userIDClaim = oauthProvider.GetUserIDClaim()
	}

	return Create(s, func() auth.Manager {
		return auth.NewManager(s.Store(),
			issuer,
			audience,
			keysLocation,
			userIDClaim,
			audiences,
			signingKeyRefreshEnabled)
	})
}

func (s *BaseServer) EphemeralManager() ephemeral.Manager {
	return Create(s, func() ephemeral.Manager {


1	package auth
2
3	import (
4		"context"
5		"crypto/sha256"
6		"encoding/base64"
7		"fmt"
8		"hash/crc32"
9
10		"github.com/golang-jwt/jwt/v5"
11
12		"github.com/netbirdio/netbird/shared/auth"
13
14		"github.com/netbirdio/netbird/base62"
15		"github.com/netbirdio/netbird/management/server/store"
16		"github.com/netbirdio/netbird/management/server/types"
17		nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
18	)
19
20	var _ Manager = (*manager)(nil)
21
22	type Manager interface {
23		ValidateAndParseToken(ctx context.Context, value string) (auth.UserAuth, *jwt.Token, error)
24		EnsureUserAccessByJWTGroups(ctx context.Context, userAuth auth.UserAuth, token *jwt.Token) (auth.UserAuth, error)
25		MarkPATUsed(ctx context.Context, tokenID string) error
26		GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
27	}
28
29	type manager struct {
30		store store.Store
31
32		validator *nbjwt.Validator
33		extractor *nbjwt.ClaimsExtractor
34	}
35
36	func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
37		// @note if invalid/missing parameters are sent the validator will instantiate
38		// but it will fail when validating and parsing the token
39		jwtValidator := nbjwt.NewValidator(
40			issuer,
41			allAudiences,
42			keysLocation,
43			idpRefreshKeys,
44		)
45
46		claimsExtractor := nbjwt.NewClaimsExtractor(
47			nbjwt.WithAudience(audience),
48			nbjwt.WithUserIDClaim(userIdClaim),
49		)
50
51		return &manager{
52			store:     store,
53			validator: jwtValidator,
54			extractor: claimsExtractor,
55		}
56	}
57
58	func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (auth.UserAuth, *jwt.Token, error) {
59		token, err := m.validator.ValidateAndParse(ctx, value)
60		if err != nil {
61			return auth.UserAuth{}, nil, err
62		}
63
64		userAuth, err := m.extractor.ToUserAuth(token)
65		if err != nil {
66			return auth.UserAuth{}, nil, err
67		}
68		return userAuth, token, err
69	}
70
71	func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth auth.UserAuth, token *jwt.Token) (auth.UserAuth, error) {
72		if userAuth.IsChild || userAuth.IsPAT {
73			return userAuth, nil
74		}
75
76		settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, userAuth.AccountId)
77		if err != nil {
78			return userAuth, err
79		}
80
81		// Ensures JWT group synchronization to the management is enabled before,
82		// filtering access based on the allowed groups.
83		if settings != nil && settings.JWTGroupsEnabled {
84			userAuth.Groups = m.extractor.ToGroups(token, settings.JWTGroupsClaimName)
85			if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
86				if !userHasAllowedGroup(allowedGroups, userAuth.Groups) {
87					return userAuth, fmt.Errorf("user does not belong to any of the allowed JWT groups")
88				}
89			}
90		}
91
92		return userAuth, nil
93	}
94
95	// MarkPATUsed marks a personal access token as used
96	func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error {
97		return am.store.MarkPATUsed(ctx, tokenID)
98	}
99
100	// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
101	func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
102		user, pat, err = am.extractPATFromToken(ctx, token)
103		if err != nil {
104			return nil, nil, "", "", err
105		}
106
107		domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthNone, user.AccountID)
108		if err != nil {
109			return nil, nil, "", "", err
110		}
111
112		return user, pat, domain, category, nil
113	}
114
115	// extractPATFromToken validates the token structure and retrieves associated User and PAT.
116	func (am *manager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
117		if len(token) != types.PATLength {
118			return nil, nil, fmt.Errorf("PAT has incorrect length")
119		}
120
121		prefix := token[:len(types.PATPrefix)]
122		if prefix != types.PATPrefix {
123			return nil, nil, fmt.Errorf("PAT has wrong prefix")
124		}
125		secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
126		encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]
127
128		verificationChecksum, err := base62.Decode(encodedChecksum)
129		if err != nil {
130			return nil, nil, fmt.Errorf("PAT checksum decoding failed: %w", err)
131		}
132
133		secretChecksum := crc32.ChecksumIEEE([]byte(secret))
134		if secretChecksum != verificationChecksum {
135			return nil, nil, fmt.Errorf("PAT checksum does not match")
136		}
137
138		hashedToken := sha256.Sum256([]byte(token))
139		encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])
140
141		var user *types.User
142		var pat *types.PersonalAccessToken
143
144		err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
145			pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthNone, encodedHashedToken)
146			if err != nil {
147				return err
148			}
149
150			user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthNone, pat.ID)
151			return err
152		})
153		if err != nil {
154			return nil, nil, err
155		}
156
157		return user, pat, nil
158	}
159
160	// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
161	func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
162		for _, userGroup := range userGroups {
163			for _, allowedGroup := range allowedGroups {
164				if userGroup == allowedGroup {
165					return true
166				}
167			}
168		}
169		return false
170	}
171

package auth

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"hash/crc32"

	"github.com/golang-jwt/jwt/v5"

	"github.com/netbirdio/netbird/shared/auth"

	"github.com/netbirdio/netbird/base62"
	"github.com/netbirdio/netbird/management/server/store"
	"github.com/netbirdio/netbird/management/server/types"
	nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
)

var _ Manager = (*manager)(nil)

type Manager interface {
	ValidateAndParseToken(ctx context.Context, value string) (auth.UserAuth, *jwt.Token, error)
	EnsureUserAccessByJWTGroups(ctx context.Context, userAuth auth.UserAuth, token *jwt.Token) (auth.UserAuth, error)
	MarkPATUsed(ctx context.Context, tokenID string) error
	GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error)
}

type manager struct {
	store store.Store

	validator *nbjwt.Validator
	extractor *nbjwt.ClaimsExtractor
}

func NewManager(store store.Store, issuer, audience, keysLocation, userIdClaim string, allAudiences []string, idpRefreshKeys bool) Manager {
	// @note if invalid/missing parameters are sent the validator will instantiate
	// but it will fail when validating and parsing the token
	jwtValidator := nbjwt.NewValidator(
		issuer,
		allAudiences,
		keysLocation,
		idpRefreshKeys,
	)

	claimsExtractor := nbjwt.NewClaimsExtractor(
		nbjwt.WithAudience(audience),
		nbjwt.WithUserIDClaim(userIdClaim),
	)

	return &manager{
		store:     store,
		validator: jwtValidator,
		extractor: claimsExtractor,
	}
}

func (m *manager) ValidateAndParseToken(ctx context.Context, value string) (auth.UserAuth, *jwt.Token, error) {
	token, err := m.validator.ValidateAndParse(ctx, value)
	if err != nil {
		return auth.UserAuth{}, nil, err
	}

	userAuth, err := m.extractor.ToUserAuth(token)
	if err != nil {
		return auth.UserAuth{}, nil, err
	}
	return userAuth, token, err
}

func (m *manager) EnsureUserAccessByJWTGroups(ctx context.Context, userAuth auth.UserAuth, token *jwt.Token) (auth.UserAuth, error) {
	if userAuth.IsChild || userAuth.IsPAT {
		return userAuth, nil
	}

	settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, userAuth.AccountId)
	if err != nil {
		return userAuth, err
	}

	// Ensures JWT group synchronization to the management is enabled before,
	// filtering access based on the allowed groups.
	if settings != nil && settings.JWTGroupsEnabled {
		userAuth.Groups = m.extractor.ToGroups(token, settings.JWTGroupsClaimName)
		if allowedGroups := settings.JWTAllowGroups; len(allowedGroups) > 0 {
			if !userHasAllowedGroup(allowedGroups, userAuth.Groups) {
				return userAuth, fmt.Errorf("user does not belong to any of the allowed JWT groups")
			}
		}
	}

	return userAuth, nil
}

// MarkPATUsed marks a personal access token as used
func (am *manager) MarkPATUsed(ctx context.Context, tokenID string) error {
	return am.store.MarkPATUsed(ctx, tokenID)
}

// GetPATInfo retrieves user, personal access token, domain, and category details from a personal access token.
func (am *manager) GetPATInfo(ctx context.Context, token string) (user *types.User, pat *types.PersonalAccessToken, domain string, category string, err error) {
	user, pat, err = am.extractPATFromToken(ctx, token)
	if err != nil {
		return nil, nil, "", "", err
	}

	domain, category, err = am.store.GetAccountDomainAndCategory(ctx, store.LockingStrengthNone, user.AccountID)
	if err != nil {
		return nil, nil, "", "", err
	}

	return user, pat, domain, category, nil
}

// extractPATFromToken validates the token structure and retrieves associated User and PAT.
func (am *manager) extractPATFromToken(ctx context.Context, token string) (*types.User, *types.PersonalAccessToken, error) {
	if len(token) != types.PATLength {
		return nil, nil, fmt.Errorf("PAT has incorrect length")
	}

	prefix := token[:len(types.PATPrefix)]
	if prefix != types.PATPrefix {
		return nil, nil, fmt.Errorf("PAT has wrong prefix")
	}
	secret := token[len(types.PATPrefix) : len(types.PATPrefix)+types.PATSecretLength]
	encodedChecksum := token[len(types.PATPrefix)+types.PATSecretLength : len(types.PATPrefix)+types.PATSecretLength+types.PATChecksumLength]

	verificationChecksum, err := base62.Decode(encodedChecksum)
	if err != nil {
		return nil, nil, fmt.Errorf("PAT checksum decoding failed: %w", err)
	}

	secretChecksum := crc32.ChecksumIEEE([]byte(secret))
	if secretChecksum != verificationChecksum {
		return nil, nil, fmt.Errorf("PAT checksum does not match")
	}

	hashedToken := sha256.Sum256([]byte(token))
	encodedHashedToken := base64.StdEncoding.EncodeToString(hashedToken[:])

	var user *types.User
	var pat *types.PersonalAccessToken

	err = am.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
		pat, err = transaction.GetPATByHashedToken(ctx, store.LockingStrengthNone, encodedHashedToken)
		if err != nil {
			return err
		}

		user, err = transaction.GetUserByPATID(ctx, store.LockingStrengthNone, pat.ID)
		return err
	})
	if err != nil {
		return nil, nil, err
	}

	return user, pat, nil
}

// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
	for _, userGroup := range userGroups {
		for _, allowedGroup := range allowedGroups {
			if userGroup == allowedGroup {
				return true
			}
		}
	}
	return false
}


Now I have the full picture. Here's the implementation:

1. **Add `GetJWKS` to Dex Provider** - reads keys from storage, converts to `Jwks` format
2. **Add `KeyFetcher` option to Validator** - alternative to HTTP-based key fetching
3. **Wire it up** in controllers for embedded IDP


Edit

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	dexapi "github.com/dexidp/dex/api/v2"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/storage/sql"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc"
)

import (
	"context"
	"crypto/ecdsa"
	"crypto/rsa"
	"encoding/base64"
	"errors"
	"fmt"
	"log/slog"
	"math/big"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	dexapi "github.com/dexidp/dex/api/v2"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/storage/sql"
	jose "github.com/go-jose/go-jose/v4"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc"

	nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
)