Files
netbird/management/server/idp/embedded_test.go
Misha Bragin 3a0cf230a1 Disable local users for a smooth single-idp mode (#5226)
Add LocalAuthDisabled option to embedded IdP configuration

This adds the ability to disable local (email/password) authentication when using the embedded Dex identity provider. When disabled, users can only authenticate via external
identity providers (Google, OIDC, etc.).

This simplifies user login when there is only one external IdP configured. The login page will redirect directly to the IdP login page.

Key changes:

Added LocalAuthDisabled field to EmbeddedIdPConfig
Added methods to check and toggle local auth: IsLocalAuthEnabled, HasNonLocalConnectors, DisableLocalAuth, EnableLocalAuth
Validation prevents disabling local auth if no external connectors are configured
Existing local users are preserved when disabled and can login again when re-enabled
Operations are idempotent (disabling already disabled is a no-op)
2026-02-01 14:26:22 +01:00

604 lines
17 KiB
Go

package idp
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/idp/dex"
)
func TestEmbeddedIdPManager_CreateUser_EndToEnd(t *testing.T) {
ctx := context.Background()
// Create a temporary directory for the test
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create the embedded IDP config
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
// Create the embedded IDP manager
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Test data
email := "newuser@example.com"
name := "New User"
accountID := "test-account-id"
invitedByEmail := "admin@example.com"
// Create the user
userData, err := manager.CreateUser(ctx, email, name, accountID, invitedByEmail)
require.NoError(t, err)
require.NotNil(t, userData)
t.Logf("Created user: ID=%s, Email=%s, Name=%s, Password=%s",
userData.ID, userData.Email, userData.Name, userData.Password)
// Verify user data
assert.Equal(t, email, userData.Email)
assert.Equal(t, name, userData.Name)
assert.NotEmpty(t, userData.ID)
assert.NotEmpty(t, userData.Password)
assert.Equal(t, accountID, userData.AppMetadata.WTAccountID)
assert.Equal(t, invitedByEmail, userData.AppMetadata.WTInvitedBy)
// Verify the user ID is in Dex's encoded format (base64 protobuf)
rawUserID, connectorID, err := dex.DecodeDexUserID(userData.ID)
require.NoError(t, err)
assert.NotEmpty(t, rawUserID)
assert.Equal(t, "local", connectorID)
t.Logf("Decoded user ID: rawUserID=%s, connectorID=%s", rawUserID, connectorID)
// Verify we can look up the user by the encoded ID
lookedUpUser, err := manager.GetUserDataByID(ctx, userData.ID, AppMetadata{WTAccountID: accountID})
require.NoError(t, err)
assert.Equal(t, email, lookedUpUser.Email)
// Verify we can look up by email
users, err := manager.GetUserByEmail(ctx, email)
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, email, users[0].Email)
// Verify creating duplicate user fails
_, err = manager.CreateUser(ctx, email, name, accountID, invitedByEmail)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already exists")
}
func TestEmbeddedIdPManager_GetUserDataByID_WithEncodedID(t *testing.T) {
ctx := context.Background()
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Create a user first
userData, err := manager.CreateUser(ctx, "test@example.com", "Test User", "account1", "admin@example.com")
require.NoError(t, err)
// The returned ID should be encoded
encodedID := userData.ID
// Lookup should work with the encoded ID
lookedUp, err := manager.GetUserDataByID(ctx, encodedID, AppMetadata{WTAccountID: "account1"})
require.NoError(t, err)
assert.Equal(t, "test@example.com", lookedUp.Email)
assert.Equal(t, "Test User", lookedUp.Name)
}
func TestEmbeddedIdPManager_DeleteUser(t *testing.T) {
ctx := context.Background()
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Create a user
userData, err := manager.CreateUser(ctx, "delete-me@example.com", "Delete Me", "account1", "admin@example.com")
require.NoError(t, err)
// Delete the user using the encoded ID
err = manager.DeleteUser(ctx, userData.ID)
require.NoError(t, err)
// Verify user no longer exists
_, err = manager.GetUserDataByID(ctx, userData.ID, AppMetadata{})
assert.Error(t, err)
}
func TestEmbeddedIdPManager_GetAccount(t *testing.T) {
ctx := context.Background()
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Create multiple users
_, err = manager.CreateUser(ctx, "user1@example.com", "User 1", "account1", "admin@example.com")
require.NoError(t, err)
_, err = manager.CreateUser(ctx, "user2@example.com", "User 2", "account1", "admin@example.com")
require.NoError(t, err)
// Get all users for the account
users, err := manager.GetAccount(ctx, "account1")
require.NoError(t, err)
assert.Len(t, users, 2)
emails := make([]string, len(users))
for i, u := range users {
emails[i] = u.Email
}
assert.Contains(t, emails, "user1@example.com")
assert.Contains(t, emails, "user2@example.com")
}
func TestEmbeddedIdPManager_UserIDFormat_MatchesJWT(t *testing.T) {
// This test verifies that the user ID returned by CreateUser
// matches the format that Dex uses in JWT tokens (the 'sub' claim)
ctx := context.Background()
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Create a user
userData, err := manager.CreateUser(ctx, "jwt-test@example.com", "JWT Test", "account1", "admin@example.com")
require.NoError(t, err)
// The ID should be in the format: base64(protobuf{user_id, connector_id})
// Example: CiQ3YWFkOGMwNS0zMjg3LTQ3M2YtYjQyYS0zNjU1MDRiZjI1ZTcSBWxvY2Fs
// Verify it can be decoded
rawUserID, connectorID, err := dex.DecodeDexUserID(userData.ID)
require.NoError(t, err)
// Raw user ID should be a UUID
assert.Regexp(t, `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, rawUserID)
// Connector ID should be "local" for password-based auth
assert.Equal(t, "local", connectorID)
// Re-encoding should produce the same result
reEncoded := dex.EncodeDexUserID(rawUserID, connectorID)
assert.Equal(t, userData.ID, reEncoded)
t.Logf("User ID format verified:")
t.Logf(" Encoded ID: %s", userData.ID)
t.Logf(" Raw UUID: %s", rawUserID)
t.Logf(" Connector: %s", connectorID)
}
func TestEmbeddedIdPManager_UpdateUserPassword(t *testing.T) {
ctx := context.Background()
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Create a user with a known password
email := "password-test@example.com"
name := "Password Test User"
initialPassword := "InitialPass123!"
userData, err := manager.CreateUserWithPassword(ctx, email, initialPassword, name)
require.NoError(t, err)
require.NotNil(t, userData)
userID := userData.ID
t.Run("successful password change", func(t *testing.T) {
newPassword := "NewSecurePass456!"
err := manager.UpdateUserPassword(ctx, userID, userID, initialPassword, newPassword)
require.NoError(t, err)
// Verify the new password works by changing it again
anotherPassword := "AnotherPass789!"
err = manager.UpdateUserPassword(ctx, userID, userID, newPassword, anotherPassword)
require.NoError(t, err)
})
t.Run("wrong old password", func(t *testing.T) {
err := manager.UpdateUserPassword(ctx, userID, userID, "wrongpassword", "NewPass123!")
require.Error(t, err)
assert.Contains(t, err.Error(), "current password is incorrect")
})
t.Run("cannot change other user password", func(t *testing.T) {
otherUserID := "other-user-id"
err := manager.UpdateUserPassword(ctx, userID, otherUserID, "oldpass", "newpass")
require.Error(t, err)
assert.Contains(t, err.Error(), "users can only change their own password")
})
t.Run("same password rejected", func(t *testing.T) {
samePassword := "SamePass123!"
err := manager.UpdateUserPassword(ctx, userID, userID, samePassword, samePassword)
require.Error(t, err)
assert.Contains(t, err.Error(), "new password must be different")
})
}
func TestEmbeddedIdPManager_GetLocalKeysLocation(t *testing.T) {
ctx := context.Background()
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
tests := []struct {
name string
localAddress string
expected string
}{
{
name: "localhost with port",
localAddress: "localhost:8080",
expected: "http://localhost:8080/oauth2/keys",
},
{
name: "localhost with https port",
localAddress: "localhost:443",
expected: "http://localhost:443/oauth2/keys",
},
{
name: "port only format",
localAddress: ":8080",
expected: "http://localhost:8080/oauth2/keys",
},
{
name: "empty address",
localAddress: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
LocalAddress: tt.localAddress,
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex-"+tt.name+".db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
result := manager.GetLocalKeysLocation()
assert.Equal(t, tt.expected, result)
})
}
}
func TestEmbeddedIdPManager_LocalAuthDisabled(t *testing.T) {
ctx := context.Background()
t.Run("cannot start with local auth disabled without other connectors", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
LocalAuthDisabled: true,
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
_, err = NewEmbeddedIdPManager(ctx, config, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "no other identity providers configured")
})
t.Run("local auth enabled by default", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
config := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: filepath.Join(tmpDir, "dex.db"),
},
},
}
manager, err := NewEmbeddedIdPManager(ctx, config, nil)
require.NoError(t, err)
defer func() { _ = manager.Stop(ctx) }()
// Verify local auth is enabled by default
assert.False(t, manager.IsLocalAuthDisabled())
})
t.Run("start with local auth disabled when connector exists", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
dbFile := filepath.Join(tmpDir, "dex.db")
// First, create a manager with local auth enabled and add a connector
config1 := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: dbFile,
},
},
}
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
require.NoError(t, err)
// Create a user
userData, err := manager1.CreateUser(ctx, "preserved@example.com", "Preserved User", "account1", "admin@example.com")
require.NoError(t, err)
userID := userData.ID
// Add an external connector (Google doesn't require OIDC discovery)
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
ID: "google-test",
Name: "Google Test",
Type: "google",
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
})
require.NoError(t, err)
// Stop the first manager
err = manager1.Stop(ctx)
require.NoError(t, err)
// Now create a new manager with local auth disabled
config2 := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
LocalAuthDisabled: true,
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: dbFile,
},
},
}
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
require.NoError(t, err)
defer func() { _ = manager2.Stop(ctx) }()
// Verify local auth is disabled via config
assert.True(t, manager2.IsLocalAuthDisabled())
// Verify the user still exists in storage (just can't login via local)
lookedUp, err := manager2.GetUserDataByID(ctx, userID, AppMetadata{})
require.NoError(t, err)
assert.Equal(t, "preserved@example.com", lookedUp.Email)
})
t.Run("CreateUser fails when local auth is disabled", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
dbFile := filepath.Join(tmpDir, "dex.db")
// First, create a manager and add an external connector
config1 := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: dbFile,
},
},
}
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
require.NoError(t, err)
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
ID: "google-test",
Name: "Google Test",
Type: "google",
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
})
require.NoError(t, err)
err = manager1.Stop(ctx)
require.NoError(t, err)
// Create manager with local auth disabled
config2 := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
LocalAuthDisabled: true,
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: dbFile,
},
},
}
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
require.NoError(t, err)
defer func() { _ = manager2.Stop(ctx) }()
// Try to create a user - should fail
_, err = manager2.CreateUser(ctx, "newuser@example.com", "New User", "account1", "admin@example.com")
require.Error(t, err)
assert.Contains(t, err.Error(), "local user creation is disabled")
})
t.Run("CreateUserWithPassword fails when local auth is disabled", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "embedded-idp-test-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
dbFile := filepath.Join(tmpDir, "dex.db")
// First, create a manager and add an external connector
config1 := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: dbFile,
},
},
}
manager1, err := NewEmbeddedIdPManager(ctx, config1, nil)
require.NoError(t, err)
_, err = manager1.CreateConnector(ctx, &dex.ConnectorConfig{
ID: "google-test",
Name: "Google Test",
Type: "google",
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
})
require.NoError(t, err)
err = manager1.Stop(ctx)
require.NoError(t, err)
// Create manager with local auth disabled
config2 := &EmbeddedIdPConfig{
Enabled: true,
Issuer: "http://localhost:5556/dex",
LocalAuthDisabled: true,
Storage: EmbeddedStorageConfig{
Type: "sqlite3",
Config: EmbeddedStorageTypeConfig{
File: dbFile,
},
},
}
manager2, err := NewEmbeddedIdPManager(ctx, config2, nil)
require.NoError(t, err)
defer func() { _ = manager2.Stop(ctx) }()
// Try to create a user with password - should fail
_, err = manager2.CreateUserWithPassword(ctx, "newuser@example.com", "SecurePass123!", "New User")
require.Error(t, err)
assert.Contains(t, err.Error(), "local user creation is disabled")
})
}