mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-28 07:19:08 +01:00
494 lines
13 KiB
Go
494 lines
13 KiB
Go
package oidcprovider
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
// Store handles persistence for OIDC provider data
|
|
type Store struct {
|
|
db *gorm.DB
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewStore creates a new Store with SQLite backend
|
|
func NewStore(ctx context.Context, dataDir string) (*Store, error) {
|
|
dbPath := fmt.Sprintf("%s/oidc.db", dataDir)
|
|
|
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open OIDC database: %w", err)
|
|
}
|
|
|
|
// Enable WAL mode for better concurrency
|
|
if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil {
|
|
log.WithContext(ctx).Warnf("failed to enable WAL mode: %v", err)
|
|
}
|
|
|
|
// Auto-migrate tables
|
|
if err := db.AutoMigrate(
|
|
&User{},
|
|
&Client{},
|
|
&AuthRequest{},
|
|
&AuthCode{},
|
|
&AccessToken{},
|
|
&RefreshToken{},
|
|
&DeviceAuth{},
|
|
&SigningKey{},
|
|
); err != nil {
|
|
return nil, fmt.Errorf("failed to migrate OIDC database: %w", err)
|
|
}
|
|
|
|
store := &Store{db: db}
|
|
|
|
// Ensure we have a signing key
|
|
if err := store.ensureSigningKey(ctx); err != nil {
|
|
return nil, fmt.Errorf("failed to ensure signing key: %w", err)
|
|
}
|
|
|
|
return store, nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (s *Store) Close() error {
|
|
sqlDB, err := s.db.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sqlDB.Close()
|
|
}
|
|
|
|
// ensureSigningKey creates a signing key if one doesn't exist
|
|
func (s *Store) ensureSigningKey(ctx context.Context) error {
|
|
var key SigningKey
|
|
err := s.db.WithContext(ctx).Where("active = ?", true).First(&key).Error
|
|
if err == nil {
|
|
return nil // Key exists
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
// Generate new RSA key pair
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate RSA key: %w", err)
|
|
}
|
|
|
|
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
})
|
|
|
|
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal public key: %w", err)
|
|
}
|
|
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PUBLIC KEY",
|
|
Bytes: publicKeyBytes,
|
|
})
|
|
|
|
newKey := &SigningKey{
|
|
ID: uuid.New().String(),
|
|
Algorithm: "RS256",
|
|
PrivateKey: privateKeyPEM,
|
|
PublicKey: publicKeyPEM,
|
|
CreatedAt: time.Now(),
|
|
Active: true,
|
|
}
|
|
|
|
return s.db.WithContext(ctx).Create(newKey).Error
|
|
}
|
|
|
|
// GetSigningKey returns the active signing key
|
|
func (s *Store) GetSigningKey(ctx context.Context) (*SigningKey, error) {
|
|
var key SigningKey
|
|
err := s.db.WithContext(ctx).Where("active = ?", true).First(&key).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &key, nil
|
|
}
|
|
|
|
// User operations
|
|
|
|
// CreateUser creates a new user with bcrypt hashed password
|
|
func (s *Store) CreateUser(ctx context.Context, user *User) error {
|
|
if user.ID == "" {
|
|
user.ID = uuid.New().String()
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
user.Password = string(hashedPassword)
|
|
user.CreatedAt = time.Now()
|
|
user.UpdatedAt = time.Now()
|
|
|
|
return s.db.WithContext(ctx).Create(user).Error
|
|
}
|
|
|
|
// GetUserByID retrieves a user by ID
|
|
func (s *Store) GetUserByID(ctx context.Context, id string) (*User, error) {
|
|
var user User
|
|
err := s.db.WithContext(ctx).Where("id = ?", id).First(&user).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// GetUserByUsername retrieves a user by username
|
|
func (s *Store) GetUserByUsername(ctx context.Context, username string) (*User, error) {
|
|
var user User
|
|
err := s.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// ValidateUserPassword validates a user's password
|
|
func (s *Store) ValidateUserPassword(ctx context.Context, username, password string) (*User, error) {
|
|
user, err := s.GetUserByUsername(ctx, username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
|
return nil, errors.New("invalid password")
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// ListUsers returns all users
|
|
func (s *Store) ListUsers(ctx context.Context) ([]*User, error) {
|
|
var users []*User
|
|
err := s.db.WithContext(ctx).Find(&users).Error
|
|
return users, err
|
|
}
|
|
|
|
// UpdateUser updates a user
|
|
func (s *Store) UpdateUser(ctx context.Context, user *User) error {
|
|
user.UpdatedAt = time.Now()
|
|
return s.db.WithContext(ctx).Save(user).Error
|
|
}
|
|
|
|
// DeleteUser deletes a user
|
|
func (s *Store) DeleteUser(ctx context.Context, id string) error {
|
|
return s.db.WithContext(ctx).Delete(&User{}, "id = ?", id).Error
|
|
}
|
|
|
|
// UpdateUserPassword updates a user's password
|
|
func (s *Store) UpdateUserPassword(ctx context.Context, id, password string) error {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
return s.db.WithContext(ctx).Model(&User{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
"password": string(hashedPassword),
|
|
"updated_at": time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
// Client operations
|
|
|
|
// CreateClient creates a new OIDC client
|
|
func (s *Store) CreateClient(ctx context.Context, client *Client) error {
|
|
if client.ID == "" {
|
|
client.ID = uuid.New().String()
|
|
}
|
|
|
|
// Hash secret if provided
|
|
if client.Secret != "" {
|
|
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(client.Secret), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash client secret: %w", err)
|
|
}
|
|
client.Secret = string(hashedSecret)
|
|
}
|
|
|
|
client.CreatedAt = time.Now()
|
|
client.UpdatedAt = time.Now()
|
|
|
|
return s.db.WithContext(ctx).Create(client).Error
|
|
}
|
|
|
|
// GetClientByID retrieves a client by ID
|
|
func (s *Store) GetClientByID(ctx context.Context, id string) (*Client, error) {
|
|
var client Client
|
|
err := s.db.WithContext(ctx).Where("id = ?", id).First(&client).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &client, nil
|
|
}
|
|
|
|
// ValidateClientSecret validates a client's secret
|
|
func (s *Store) ValidateClientSecret(ctx context.Context, clientID, secret string) (*Client, error) {
|
|
client, err := s.GetClientByID(ctx, clientID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Public clients have no secret
|
|
if client.Secret == "" && secret == "" {
|
|
return client, nil
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(secret)); err != nil {
|
|
return nil, errors.New("invalid client secret")
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// ListClients returns all clients
|
|
func (s *Store) ListClients(ctx context.Context) ([]*Client, error) {
|
|
var clients []*Client
|
|
err := s.db.WithContext(ctx).Find(&clients).Error
|
|
return clients, err
|
|
}
|
|
|
|
// DeleteClient deletes a client
|
|
func (s *Store) DeleteClient(ctx context.Context, id string) error {
|
|
return s.db.WithContext(ctx).Delete(&Client{}, "id = ?", id).Error
|
|
}
|
|
|
|
// AuthRequest operations
|
|
|
|
// SaveAuthRequest saves an authorization request
|
|
func (s *Store) SaveAuthRequest(ctx context.Context, req *AuthRequest) error {
|
|
if req.ID == "" {
|
|
req.ID = uuid.New().String()
|
|
}
|
|
req.CreatedAt = time.Now()
|
|
return s.db.WithContext(ctx).Create(req).Error
|
|
}
|
|
|
|
// GetAuthRequestByID retrieves an auth request by ID
|
|
func (s *Store) GetAuthRequestByID(ctx context.Context, id string) (*AuthRequest, error) {
|
|
var req AuthRequest
|
|
err := s.db.WithContext(ctx).Where("id = ?", id).First(&req).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &req, nil
|
|
}
|
|
|
|
// UpdateAuthRequest updates an auth request
|
|
func (s *Store) UpdateAuthRequest(ctx context.Context, req *AuthRequest) error {
|
|
return s.db.WithContext(ctx).Save(req).Error
|
|
}
|
|
|
|
// DeleteAuthRequest deletes an auth request
|
|
func (s *Store) DeleteAuthRequest(ctx context.Context, id string) error {
|
|
return s.db.WithContext(ctx).Delete(&AuthRequest{}, "id = ?", id).Error
|
|
}
|
|
|
|
// AuthCode operations
|
|
|
|
// SaveAuthCode saves an authorization code
|
|
func (s *Store) SaveAuthCode(ctx context.Context, code *AuthCode) error {
|
|
code.CreatedAt = time.Now()
|
|
if code.ExpiresAt.IsZero() {
|
|
code.ExpiresAt = time.Now().Add(10 * time.Minute) // 10 minute expiry
|
|
}
|
|
return s.db.WithContext(ctx).Create(code).Error
|
|
}
|
|
|
|
// GetAuthCodeByCode retrieves an auth code
|
|
func (s *Store) GetAuthCodeByCode(ctx context.Context, code string) (*AuthCode, error) {
|
|
var authCode AuthCode
|
|
err := s.db.WithContext(ctx).Where("code = ?", code).First(&authCode).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &authCode, nil
|
|
}
|
|
|
|
// DeleteAuthCode deletes an auth code
|
|
func (s *Store) DeleteAuthCode(ctx context.Context, code string) error {
|
|
return s.db.WithContext(ctx).Delete(&AuthCode{}, "code = ?", code).Error
|
|
}
|
|
|
|
// Token operations
|
|
|
|
// SaveAccessToken saves an access token
|
|
func (s *Store) SaveAccessToken(ctx context.Context, token *AccessToken) error {
|
|
if token.ID == "" {
|
|
token.ID = uuid.New().String()
|
|
}
|
|
token.CreatedAt = time.Now()
|
|
return s.db.WithContext(ctx).Create(token).Error
|
|
}
|
|
|
|
// GetAccessTokenByID retrieves an access token
|
|
func (s *Store) GetAccessTokenByID(ctx context.Context, id string) (*AccessToken, error) {
|
|
var token AccessToken
|
|
err := s.db.WithContext(ctx).Where("id = ?", id).First(&token).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &token, nil
|
|
}
|
|
|
|
// DeleteAccessToken deletes an access token
|
|
func (s *Store) DeleteAccessToken(ctx context.Context, id string) error {
|
|
return s.db.WithContext(ctx).Delete(&AccessToken{}, "id = ?", id).Error
|
|
}
|
|
|
|
// RefreshToken operations
|
|
|
|
// SaveRefreshToken saves a refresh token
|
|
func (s *Store) SaveRefreshToken(ctx context.Context, token *RefreshToken) error {
|
|
if token.ID == "" {
|
|
token.ID = uuid.New().String()
|
|
}
|
|
if token.Token == "" {
|
|
token.Token = uuid.New().String()
|
|
}
|
|
token.CreatedAt = time.Now()
|
|
return s.db.WithContext(ctx).Create(token).Error
|
|
}
|
|
|
|
// GetRefreshToken retrieves a refresh token by token value
|
|
func (s *Store) GetRefreshToken(ctx context.Context, token string) (*RefreshToken, error) {
|
|
var rt RefreshToken
|
|
err := s.db.WithContext(ctx).Where("token = ?", token).First(&rt).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &rt, nil
|
|
}
|
|
|
|
// DeleteRefreshToken deletes a refresh token
|
|
func (s *Store) DeleteRefreshToken(ctx context.Context, id string) error {
|
|
return s.db.WithContext(ctx).Delete(&RefreshToken{}, "id = ?", id).Error
|
|
}
|
|
|
|
// DeleteRefreshTokenByToken deletes a refresh token by token value
|
|
func (s *Store) DeleteRefreshTokenByToken(ctx context.Context, token string) error {
|
|
return s.db.WithContext(ctx).Delete(&RefreshToken{}, "token = ?", token).Error
|
|
}
|
|
|
|
// DeviceAuth operations
|
|
|
|
// SaveDeviceAuth saves a device authorization
|
|
func (s *Store) SaveDeviceAuth(ctx context.Context, auth *DeviceAuth) error {
|
|
auth.CreatedAt = time.Now()
|
|
return s.db.WithContext(ctx).Create(auth).Error
|
|
}
|
|
|
|
// GetDeviceAuthByDeviceCode retrieves device auth by device code
|
|
func (s *Store) GetDeviceAuthByDeviceCode(ctx context.Context, deviceCode string) (*DeviceAuth, error) {
|
|
var auth DeviceAuth
|
|
err := s.db.WithContext(ctx).Where("device_code = ?", deviceCode).First(&auth).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &auth, nil
|
|
}
|
|
|
|
// GetDeviceAuthByUserCode retrieves device auth by user code
|
|
func (s *Store) GetDeviceAuthByUserCode(ctx context.Context, userCode string) (*DeviceAuth, error) {
|
|
var auth DeviceAuth
|
|
err := s.db.WithContext(ctx).Where("user_code = ?", userCode).First(&auth).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &auth, nil
|
|
}
|
|
|
|
// UpdateDeviceAuth updates a device authorization
|
|
func (s *Store) UpdateDeviceAuth(ctx context.Context, auth *DeviceAuth) error {
|
|
return s.db.WithContext(ctx).Save(auth).Error
|
|
}
|
|
|
|
// DeleteDeviceAuth deletes a device authorization
|
|
func (s *Store) DeleteDeviceAuth(ctx context.Context, deviceCode string) error {
|
|
return s.db.WithContext(ctx).Delete(&DeviceAuth{}, "device_code = ?", deviceCode).Error
|
|
}
|
|
|
|
// Cleanup operations
|
|
|
|
// CleanupExpired removes expired tokens and auth requests
|
|
func (s *Store) CleanupExpired(ctx context.Context) error {
|
|
now := time.Now()
|
|
|
|
// Delete expired auth codes
|
|
if err := s.db.WithContext(ctx).Delete(&AuthCode{}, "expires_at < ?", now).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete expired access tokens
|
|
if err := s.db.WithContext(ctx).Delete(&AccessToken{}, "expiration < ?", now).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete expired refresh tokens
|
|
if err := s.db.WithContext(ctx).Delete(&RefreshToken{}, "expiration < ?", now).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete expired device authorizations
|
|
if err := s.db.WithContext(ctx).Delete(&DeviceAuth{}, "expiration < ?", now).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete old auth requests (older than 1 hour)
|
|
oneHourAgo := now.Add(-1 * time.Hour)
|
|
if err := s.db.WithContext(ctx).Delete(&AuthRequest{}, "created_at < ?", oneHourAgo).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions for JSON serialization
|
|
|
|
// ParseJSONArray parses a JSON array string into a slice
|
|
func ParseJSONArray(jsonStr string) []string {
|
|
if jsonStr == "" {
|
|
return nil
|
|
}
|
|
var result []string
|
|
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ToJSONArray converts a slice to a JSON array string
|
|
func ToJSONArray(arr []string) string {
|
|
if len(arr) == 0 {
|
|
return "[]"
|
|
}
|
|
data, err := json.Marshal(arr)
|
|
if err != nil {
|
|
return "[]"
|
|
}
|
|
return string(data)
|
|
}
|