fixed logout showing non actionable error and session cookie encription key

This commit is contained in:
jnfrati
2026-04-29 13:39:39 +02:00
parent ca80057ca5
commit fa0b7bece8
7 changed files with 171 additions and 23 deletions

View File

@@ -2,6 +2,7 @@ package idp
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
@@ -18,12 +19,13 @@ import (
)
const (
staticClientDashboard = "netbird-dashboard"
staticClientCLI = "netbird-cli"
defaultCLIRedirectURL1 = "http://localhost:53000/"
defaultCLIRedirectURL2 = "http://localhost:54000/"
defaultScopes = "openid profile email groups"
defaultUserIDClaim = "sub"
staticClientDashboard = "netbird-dashboard"
staticClientCLI = "netbird-cli"
defaultCLIRedirectURL1 = "http://localhost:53000/"
defaultCLIRedirectURL2 = "http://localhost:54000/"
defaultScopes = "openid profile email groups"
defaultUserIDClaim = "sub"
sessionCookieEncryptionKeyEnv = "NB_IDP_SESSION_COOKIE_ENCRYPTION_KEY"
)
// EmbeddedIdPConfig contains configuration for the embedded Dex OIDC identity provider
@@ -60,6 +62,10 @@ type EmbeddedIdPConfig struct {
// login screen. When true, the session cookie persists across browser tabs/restarts so
// MFA is not re-prompted until the session expires. Defaults to false.
MfaSessionRememberMe bool
// SessionCookieEncryptionKey is the optional AES key used to encrypt embedded IdP session cookies.
// It can also be set with NB_IDP_SESSION_COOKIE_ENCRYPTION_KEY. The value must be 16, 24, or 32
// bytes when provided as a raw string, or base64-encoded to one of those lengths.
SessionCookieEncryptionKey string
// Dashboard Post logout redirect URIs, these are required to tell
// Dex what to allow when an RP-Initiated logout is started by the frontend
// at least one of these must match the dashboard base URL or the dashboard
@@ -188,7 +194,7 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
// Always initialize MFA providers and sessions so TOTP can be toggled at runtime.
// MFAChain on clients is NOT set here — it's synced from the DB setting on startup.
if err := configureMFA(cfg, c.MfaSessionMaxLifetime, c.MfaSessionIdleTimeout, c.MfaSessionRememberMe); err != nil {
if err := configureMFA(cfg, c.MfaSessionMaxLifetime, c.MfaSessionIdleTimeout, c.MfaSessionRememberMe, c.SessionCookieEncryptionKey); err != nil {
return nil, err
}
@@ -229,7 +235,7 @@ func sanitizePostLogoutRedirectURIs(uris []string) []string {
return result
}
func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout string, rememberMe bool) error {
func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout string, rememberMe bool, sessionCookieEncryptionKey string) error {
cfg.MFA.Authenticators = []dex.MFAAuthenticator{{
ID: "default-totp",
// Has to be caps otherwise it will fail
@@ -247,13 +253,18 @@ func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout st
sessionIdleTimeout = "1h"
}
cookieEncryptionKey, err := resolveSessionCookieEncryptionKey(sessionCookieEncryptionKey)
if err != nil {
return err
}
cfg.Sessions = &dex.Sessions{
CookieName: "netbird-session",
AbsoluteLifetime: sessionMaxLifetime,
ValidIfNotUsedFor: sessionIdleTimeout,
RememberMeCheckedByDefault: &rememberMe,
SSOSharedWithDefault: "all",
CookieEncryptionKey: "32",
CookieEncryptionKey: cookieEncryptionKey,
}
// Absolutely required, otherwise the dex server will omit the MFA configuration entirely
os.Setenv("DEX_SESSIONS_ENABLED", "true")
@@ -263,6 +274,43 @@ func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout st
return nil
}
func resolveSessionCookieEncryptionKey(configuredKey string) (string, error) {
key := strings.TrimSpace(configuredKey)
if key == "" {
key = strings.TrimSpace(os.Getenv(sessionCookieEncryptionKeyEnv))
}
if key == "" {
return "", nil
}
if validSessionCookieEncryptionKeyLength(len([]byte(key))) {
return key, nil
}
for _, encoding := range []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
} {
decoded, err := encoding.DecodeString(key)
if err == nil && validSessionCookieEncryptionKeyLength(len(decoded)) {
return string(decoded), nil
}
}
return "", fmt.Errorf("invalid embedded IdP session cookie encryption key: %s (or sessionCookieEncryptionKey) must be 16, 24, or 32 bytes as a raw string or base64-encoded to one of those lengths; got %d raw bytes", sessionCookieEncryptionKeyEnv, len([]byte(key)))
}
func validSessionCookieEncryptionKeyLength(length int) bool {
switch length {
case 16, 24, 32:
return true
default:
return false
}
}
// Compile-time check that EmbeddedIdPManager implements Manager interface
var _ Manager = (*EmbeddedIdPManager)(nil)

View File

@@ -2,6 +2,7 @@ package idp
import (
"context"
"encoding/base64"
"os"
"path/filepath"
"testing"
@@ -313,6 +314,72 @@ func TestEmbeddedIdPManager_UpdateUserPassword(t *testing.T) {
})
}
func TestEmbeddedIdPConfig_ToYAMLConfig_SessionCookieEncryptionKey(t *testing.T) {
t.Setenv(sessionCookieEncryptionKeyEnv, "")
rawKey := "0123456789abcdef0123456789abcdef"
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
SessionCookieEncryptionKey: base64.StdEncoding.EncodeToString([]byte(rawKey)),
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(t.TempDir(), "dex.db"),
},
},
}
yamlConfig, err := config.ToYAMLConfig()
require.NoError(t, err)
require.NotNil(t, yamlConfig.Sessions)
assert.Equal(t, rawKey, yamlConfig.Sessions.CookieEncryptionKey)
}
func TestResolveSessionCookieEncryptionKey(t *testing.T) {
rawKey := "0123456789abcdef0123456789abcdef"
t.Run("uses raw configured key", func(t *testing.T) {
t.Setenv(sessionCookieEncryptionKeyEnv, "")
key, err := resolveSessionCookieEncryptionKey(rawKey)
require.NoError(t, err)
assert.Equal(t, rawKey, key)
})
t.Run("uses base64 configured key", func(t *testing.T) {
t.Setenv(sessionCookieEncryptionKeyEnv, "")
key, err := resolveSessionCookieEncryptionKey(base64.StdEncoding.EncodeToString([]byte(rawKey)))
require.NoError(t, err)
assert.Equal(t, rawKey, key)
})
t.Run("falls back to env var", func(t *testing.T) {
t.Setenv(sessionCookieEncryptionKeyEnv, rawKey)
key, err := resolveSessionCookieEncryptionKey("")
require.NoError(t, err)
assert.Equal(t, rawKey, key)
})
t.Run("empty key disables encryption", func(t *testing.T) {
t.Setenv(sessionCookieEncryptionKeyEnv, "")
key, err := resolveSessionCookieEncryptionKey("")
require.NoError(t, err)
assert.Empty(t, key)
})
t.Run("rejects invalid key length", func(t *testing.T) {
t.Setenv(sessionCookieEncryptionKeyEnv, "")
_, err := resolveSessionCookieEncryptionKey("32")
require.Error(t, err)
assert.Contains(t, err.Error(), sessionCookieEncryptionKeyEnv)
})
}
func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) {
ctx := context.Background()