mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-16 04:56:12 +00:00
feat: encrypt private keys saved on disk and in database (#682)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
committed by
GitHub
parent
9872608d61
commit
5550729120
@@ -4,10 +4,12 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
||||
)
|
||||
|
||||
// NewTestAppConfigService is a function used by tests to create AppConfigService objects with pre-defined configuration values
|
||||
@@ -22,7 +24,7 @@ func NewTestAppConfigService(config *model.AppConfig) *AppConfigService {
|
||||
|
||||
func TestLoadDbConfig(t *testing.T) {
|
||||
t.Run("empty config table", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
service := &AppConfigService{
|
||||
db: db,
|
||||
}
|
||||
@@ -36,7 +38,7 @@ func TestLoadDbConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("loads value from config table", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Populate the config table with some initial values
|
||||
err := db.
|
||||
@@ -66,7 +68,7 @@ func TestLoadDbConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ignores unknown config keys", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Add an entry with a key that doesn't exist in the config struct
|
||||
err := db.Create([]model.AppConfigVariable{
|
||||
@@ -87,7 +89,7 @@ func TestLoadDbConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("loading config multiple times", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Initial state
|
||||
err := db.Create([]model.AppConfigVariable{
|
||||
@@ -129,7 +131,7 @@ func TestLoadDbConfig(t *testing.T) {
|
||||
common.EnvConfig.UiConfigDisabled = true
|
||||
|
||||
// Create database with config that should be ignored
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
err := db.Create([]model.AppConfigVariable{
|
||||
{Key: "appName", Value: "DB App"},
|
||||
{Key: "sessionDuration", Value: "120"},
|
||||
@@ -165,7 +167,7 @@ func TestLoadDbConfig(t *testing.T) {
|
||||
common.EnvConfig.UiConfigDisabled = false
|
||||
|
||||
// Create database with config values that should take precedence
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
err := db.Create([]model.AppConfigVariable{
|
||||
{Key: "appName", Value: "DB App"},
|
||||
{Key: "sessionDuration", Value: "120"},
|
||||
@@ -189,7 +191,7 @@ func TestLoadDbConfig(t *testing.T) {
|
||||
|
||||
func TestUpdateAppConfigValues(t *testing.T) {
|
||||
t.Run("update single value", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
@@ -214,7 +216,7 @@ func TestUpdateAppConfigValues(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("update multiple values", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
@@ -258,7 +260,7 @@ func TestUpdateAppConfigValues(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("empty value resets to default", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
@@ -279,7 +281,7 @@ func TestUpdateAppConfigValues(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("error with odd number of arguments", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
@@ -295,7 +297,7 @@ func TestUpdateAppConfigValues(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("error with invalid key", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
@@ -313,7 +315,7 @@ func TestUpdateAppConfigValues(t *testing.T) {
|
||||
|
||||
func TestUpdateAppConfig(t *testing.T) {
|
||||
t.Run("updates configuration values from DTO", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
@@ -386,7 +388,7 @@ func TestUpdateAppConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("empty values reset to defaults", func(t *testing.T) {
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config and modify some values
|
||||
service := &AppConfigService{
|
||||
@@ -451,7 +453,7 @@ func TestUpdateAppConfig(t *testing.T) {
|
||||
// Disable UI config
|
||||
common.EnvConfig.UiConfigDisabled = true
|
||||
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
service := &AppConfigService{
|
||||
db: db,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
"gorm.io/gorm"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
|
||||
@@ -60,7 +62,7 @@ func (s *TestService) initExternalIdP() error {
|
||||
return fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
s.externalIdPKey, err = utils.ImportRawKey(rawKey)
|
||||
s.externalIdPKey, err = jwkutils.ImportRawKey(rawKey, jwa.ES256().String(), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import private key: %w", err)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,20 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,8 +23,9 @@ const (
|
||||
// This is a JSON file containing a key encoded as JWK
|
||||
PrivateKeyFile = "jwt_private_key.json"
|
||||
|
||||
// RsaKeySize is the size, in bits, of the RSA key to generate if none is found
|
||||
RsaKeySize = 2048
|
||||
// PrivateKeyFileEncrypted is the path in the data/keys folder where the encrypted key is stored
|
||||
// This is a encrypted JSON file containing a key encoded as JWK
|
||||
PrivateKeyFileEncrypted = "jwt_private_key.json.enc"
|
||||
|
||||
// KeyUsageSigning is the usage for the private keys, for the "use" property
|
||||
KeyUsageSigning = "sig"
|
||||
@@ -59,58 +57,74 @@ const (
|
||||
)
|
||||
|
||||
type JwtService struct {
|
||||
envConfig *common.EnvConfigSchema
|
||||
privateKey jwk.Key
|
||||
keyId string
|
||||
appConfigService *AppConfigService
|
||||
jwksEncoded []byte
|
||||
}
|
||||
|
||||
func NewJwtService(appConfigService *AppConfigService) *JwtService {
|
||||
func NewJwtService(db *gorm.DB, appConfigService *AppConfigService) *JwtService {
|
||||
service := &JwtService{}
|
||||
|
||||
// Ensure keys are generated or loaded
|
||||
if err := service.init(appConfigService, common.EnvConfig.KeysPath); err != nil {
|
||||
err := service.init(db, appConfigService, &common.EnvConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize jwt service: %v", err)
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *JwtService) init(appConfigService *AppConfigService, keysPath string) error {
|
||||
func (s *JwtService) init(db *gorm.DB, appConfigService *AppConfigService, envConfig *common.EnvConfigSchema) (err error) {
|
||||
s.appConfigService = appConfigService
|
||||
s.envConfig = envConfig
|
||||
|
||||
// Ensure keys are generated or loaded
|
||||
return s.loadOrGenerateKey(keysPath)
|
||||
return s.loadOrGenerateKey(db)
|
||||
}
|
||||
|
||||
// loadOrGenerateKey loads the private key from the given path or generates it if not existing.
|
||||
func (s *JwtService) loadOrGenerateKey(keysPath string) error {
|
||||
var key jwk.Key
|
||||
|
||||
// First, check if we have a JWK file
|
||||
// If we do, then we just load that
|
||||
jwkPath := filepath.Join(keysPath, PrivateKeyFile)
|
||||
ok, err := utils.FileExists(jwkPath)
|
||||
func (s *JwtService) loadOrGenerateKey(db *gorm.DB) error {
|
||||
// Get the key provider
|
||||
keyProvider, err := jwkutils.GetKeyProvider(db, s.envConfig, s.appConfigService.GetDbConfig().InstanceID.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if private key file (JWK) exists at path '%s': %w", jwkPath, err)
|
||||
return fmt.Errorf("failed to get key provider: %w", err)
|
||||
}
|
||||
if ok {
|
||||
key, err = s.loadKeyJWK(jwkPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load private key file (JWK) at path '%s': %w", jwkPath, err)
|
||||
}
|
||||
|
||||
// Set the key, and we are done
|
||||
// Try loading a key
|
||||
key, err := keyProvider.LoadKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load key (provider type '%s'): %w", s.envConfig.KeysStorage, err)
|
||||
}
|
||||
|
||||
// If we have a key, store it in the object and we're done
|
||||
if key != nil {
|
||||
err = s.SetKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set private key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we are here, we need to generate a new key
|
||||
key, err = s.generateNewRSAKey()
|
||||
err = s.generateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
|
||||
// Save the newly-generated key
|
||||
err = keyProvider.SaveKey(s.privateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save private key (provider type '%s'): %w", s.envConfig.KeysStorage, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateKey generates a new key and stores it in the object
|
||||
func (s *JwtService) generateKey() error {
|
||||
// Default is to generate RS256 (RSA-2048) keys
|
||||
key, err := jwkutils.GenerateKey(jwa.RS256().String(), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate new private key: %w", err)
|
||||
}
|
||||
@@ -121,12 +135,6 @@ func (s *JwtService) loadOrGenerateKey(keysPath string) error {
|
||||
return fmt.Errorf("failed to set private key: %w", err)
|
||||
}
|
||||
|
||||
// Save the key as JWK
|
||||
err = SaveKeyJWK(s.privateKey, jwkPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save private key file at path '%s': %w", jwkPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -192,13 +200,13 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
||||
Subject(user.ID).
|
||||
Expiration(now.Add(s.appConfigService.GetDbConfig().SessionDuration.AsDurationMinutes())).
|
||||
IssuedAt(now).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(s.envConfig.AppURL).
|
||||
Build()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build token: %w", err)
|
||||
}
|
||||
|
||||
err = SetAudienceString(token, common.EnvConfig.AppURL)
|
||||
err = SetAudienceString(token, s.envConfig.AppURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to set 'aud' claim in token: %w", err)
|
||||
}
|
||||
@@ -229,8 +237,8 @@ func (s *JwtService) VerifyAccessToken(tokenString string) (jwt.Token, error) {
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithKey(alg, s.privateKey),
|
||||
jwt.WithAcceptableSkew(clockSkew),
|
||||
jwt.WithAudience(common.EnvConfig.AppURL),
|
||||
jwt.WithIssuer(common.EnvConfig.AppURL),
|
||||
jwt.WithAudience(s.envConfig.AppURL),
|
||||
jwt.WithIssuer(s.envConfig.AppURL),
|
||||
jwt.WithValidator(TokenTypeValidator(AccessTokenJWTType)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -246,7 +254,7 @@ func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, no
|
||||
token, err := jwt.NewBuilder().
|
||||
Expiration(now.Add(1 * time.Hour)).
|
||||
IssuedAt(now).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(s.envConfig.AppURL).
|
||||
Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build token: %w", err)
|
||||
@@ -305,7 +313,7 @@ func (s *JwtService) VerifyIdToken(tokenString string, acceptExpiredTokens bool)
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithKey(alg, s.privateKey),
|
||||
jwt.WithAcceptableSkew(clockSkew),
|
||||
jwt.WithIssuer(common.EnvConfig.AppURL),
|
||||
jwt.WithIssuer(s.envConfig.AppURL),
|
||||
jwt.WithValidator(TokenTypeValidator(IDTokenJWTType)),
|
||||
)
|
||||
|
||||
@@ -335,7 +343,7 @@ func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string) (jw
|
||||
Subject(user.ID).
|
||||
Expiration(now.Add(1 * time.Hour)).
|
||||
IssuedAt(now).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(s.envConfig.AppURL).
|
||||
Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build token: %w", err)
|
||||
@@ -377,7 +385,7 @@ func (s *JwtService) VerifyOAuthAccessToken(tokenString string) (jwt.Token, erro
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithKey(alg, s.privateKey),
|
||||
jwt.WithAcceptableSkew(clockSkew),
|
||||
jwt.WithIssuer(common.EnvConfig.AppURL),
|
||||
jwt.WithIssuer(s.envConfig.AppURL),
|
||||
jwt.WithValidator(TokenTypeValidator(OAuthAccessTokenJWTType)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -393,7 +401,7 @@ func (s *JwtService) GenerateOAuthRefreshToken(userID string, clientID string, r
|
||||
Subject(userID).
|
||||
Expiration(now.Add(RefreshTokenDuration)).
|
||||
IssuedAt(now).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(s.envConfig.AppURL).
|
||||
Build()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build token: %w", err)
|
||||
@@ -430,7 +438,7 @@ func (s *JwtService) VerifyOAuthRefreshToken(tokenString string) (userID, client
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithKey(alg, s.privateKey),
|
||||
jwt.WithAcceptableSkew(clockSkew),
|
||||
jwt.WithIssuer(common.EnvConfig.AppURL),
|
||||
jwt.WithIssuer(s.envConfig.AppURL),
|
||||
jwt.WithValidator(TokenTypeValidator(OAuthRefreshTokenJWTType)),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -488,7 +496,7 @@ func (s *JwtService) GetPublicJWK() (jwk.Key, error) {
|
||||
return nil, fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
|
||||
utils.EnsureAlgInKey(pubKey)
|
||||
jwkutils.EnsureAlgInKey(pubKey, "", "")
|
||||
|
||||
return pubKey, nil
|
||||
}
|
||||
@@ -517,56 +525,6 @@ func (s *JwtService) GetKeyAlg() (jwa.KeyAlgorithm, error) {
|
||||
return alg, nil
|
||||
}
|
||||
|
||||
func (s *JwtService) loadKeyJWK(path string) (jwk.Key, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read key data: %w", err)
|
||||
}
|
||||
|
||||
key, err := jwk.ParseKey(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (s *JwtService) generateNewRSAKey() (jwk.Key, error) {
|
||||
// We generate RSA keys only
|
||||
rawKey, err := rsa.GenerateKey(rand.Reader, RsaKeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate RSA private key: %w", err)
|
||||
}
|
||||
|
||||
// Import the raw key
|
||||
return utils.ImportRawKey(rawKey)
|
||||
}
|
||||
|
||||
// SaveKeyJWK saves a JWK to a file
|
||||
func SaveKeyJWK(key jwk.Key, path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory '%s' for key file: %w", dir, err)
|
||||
}
|
||||
|
||||
keyFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create key file: %w", err)
|
||||
}
|
||||
defer keyFile.Close()
|
||||
|
||||
// Write the JSON file to disk
|
||||
enc := json.NewEncoder(keyFile)
|
||||
enc.SetEscapeHTML(false)
|
||||
err = enc.Encode(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIsAdmin returns the value of the "isAdmin" claim in the token
|
||||
func GetIsAdmin(token jwt.Token) (bool, error) {
|
||||
if !token.Has(IsAdminClaim) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
|
||||
)
|
||||
|
||||
func TestJwtService_Init(t *testing.T) {
|
||||
@@ -33,9 +33,16 @@ func TestJwtService_Init(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// Initialize the JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify the private key was set
|
||||
@@ -66,9 +73,16 @@ func TestJwtService_Init(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// First create a service to generate a key
|
||||
firstService := &JwtService{}
|
||||
err := firstService.init(mockConfig, tempDir)
|
||||
err := firstService.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the key ID of the first service
|
||||
@@ -77,7 +91,7 @@ func TestJwtService_Init(t *testing.T) {
|
||||
|
||||
// Now create a new service that should load the existing key
|
||||
secondService := &JwtService{}
|
||||
err = secondService.init(mockConfig, tempDir)
|
||||
err = secondService.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the loaded key has the same ID as the original
|
||||
@@ -90,12 +104,19 @@ func TestJwtService_Init(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// Create a new JWK and save it to disk
|
||||
origKeyID := createECDSAKeyJWK(t, tempDir)
|
||||
|
||||
// Now create a new service that should load the existing key
|
||||
svc := &JwtService{}
|
||||
err := svc.init(mockConfig, tempDir)
|
||||
err := svc.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure loaded key has the right algorithm
|
||||
@@ -113,12 +134,19 @@ func TestJwtService_Init(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// Create a new JWK and save it to disk
|
||||
origKeyID := createEdDSAKeyJWK(t, tempDir)
|
||||
|
||||
// Now create a new service that should load the existing key
|
||||
svc := &JwtService{}
|
||||
err := svc.init(mockConfig, tempDir)
|
||||
err := svc.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure loaded key has the right algorithm and curve
|
||||
@@ -147,9 +175,16 @@ func TestJwtService_GetPublicJWK(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// Create a JWT service with initialized key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Get the JWK (public key)
|
||||
@@ -178,12 +213,19 @@ func TestJwtService_GetPublicJWK(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// Create an ECDSA key and save it as JWK
|
||||
originalKeyID := createECDSAKeyJWK(t, tempDir)
|
||||
|
||||
// Create a JWT service that loads the ECDSA key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Get the JWK (public key)
|
||||
@@ -216,12 +258,19 @@ func TestJwtService_GetPublicJWK(t *testing.T) {
|
||||
// Create a temporary directory for the test
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
// Create an EdDSA key and save it as JWK
|
||||
originalKeyID := createEdDSAKeyJWK(t, tempDir)
|
||||
|
||||
// Create a JWT service that loads the EdDSA key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Get the JWK (public key)
|
||||
@@ -276,16 +325,16 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
})
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
originalAppURL := common.EnvConfig.AppURL
|
||||
common.EnvConfig.AppURL = "https://test.example.com"
|
||||
defer func() {
|
||||
common.EnvConfig.AppURL = originalAppURL
|
||||
}()
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
t.Run("generates token for regular user", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create a test user
|
||||
@@ -328,7 +377,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
t.Run("generates token for admin user", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create a test admin user
|
||||
@@ -364,7 +413,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
})
|
||||
|
||||
service := &JwtService{}
|
||||
err := service.init(customMockConfig, tempDir)
|
||||
err := service.init(nil, customMockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create a test user
|
||||
@@ -399,7 +448,10 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -453,7 +505,10 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -507,7 +562,10 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -563,16 +621,16 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
})
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
originalAppURL := common.EnvConfig.AppURL
|
||||
common.EnvConfig.AppURL = "https://test.example.com"
|
||||
defer func() {
|
||||
common.EnvConfig.AppURL = originalAppURL
|
||||
}()
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
t.Run("generates and verifies ID token with standard claims", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create test claims
|
||||
@@ -601,7 +659,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
assert.Equal(t, []string{clientID}, audience, "Audience should contain the client ID")
|
||||
issuer, ok := claims.Issuer()
|
||||
_ = assert.True(t, ok, "Issuer not found in token") &&
|
||||
assert.Equal(t, common.EnvConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
|
||||
// Check token expiration time is approximately 1 hour from now
|
||||
expectedExp := time.Now().Add(1 * time.Hour)
|
||||
@@ -614,7 +672,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
t.Run("can accept expired tokens if told so", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create test claims
|
||||
@@ -628,7 +686,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
// Create a token that's already expired
|
||||
token, err := jwt.NewBuilder().
|
||||
Subject(userClaims["sub"].(string)).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(service.envConfig.AppURL).
|
||||
Audience([]string{clientID}).
|
||||
IssuedAt(time.Now().Add(-2 * time.Hour)).
|
||||
Expiration(time.Now().Add(-1 * time.Hour)). // Expired 1 hour ago
|
||||
@@ -666,13 +724,13 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
assert.Equal(t, userClaims["sub"], subject, "Token subject should match user ID")
|
||||
issuer, ok := claims.Issuer()
|
||||
_ = assert.True(t, ok, "Issuer not found in token") &&
|
||||
assert.Equal(t, common.EnvConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
})
|
||||
|
||||
t.Run("generates and verifies ID token with nonce", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create test claims with nonce
|
||||
@@ -703,7 +761,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
t.Run("fails verification with incorrect issuer", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Generate a token with standard claims
|
||||
@@ -714,7 +772,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
require.NoError(t, err, "Failed to generate ID token")
|
||||
|
||||
// Temporarily change the app URL to simulate wrong issuer
|
||||
common.EnvConfig.AppURL = "https://wrong-issuer.com"
|
||||
service.envConfig.AppURL = "https://wrong-issuer.com"
|
||||
|
||||
// Verify should fail due to issuer mismatch
|
||||
_, err = service.VerifyIdToken(tokenString, false)
|
||||
@@ -731,7 +789,10 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -762,7 +823,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
assert.Equal(t, "eddsauser456", subject, "Token subject should match user ID")
|
||||
issuer, ok := claims.Issuer()
|
||||
_ = assert.True(t, ok, "Issuer not found in token") &&
|
||||
assert.Equal(t, common.EnvConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
|
||||
// Verify the key type is OKP
|
||||
publicKey, err := service.GetPublicJWK()
|
||||
@@ -784,7 +845,10 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -795,7 +859,6 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
// Create test claims
|
||||
userClaims := map[string]interface{}{
|
||||
"sub": "ecdsauser456",
|
||||
"name": "ECDSA User",
|
||||
"email": "ecdsauser@example.com",
|
||||
}
|
||||
const clientID = "ecdsa-client-123"
|
||||
@@ -815,7 +878,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
assert.Equal(t, "ecdsauser456", subject, "Token subject should match user ID")
|
||||
issuer, ok := claims.Issuer()
|
||||
_ = assert.True(t, ok, "Issuer not found in token") &&
|
||||
assert.Equal(t, common.EnvConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
|
||||
// Verify the key type is EC
|
||||
publicKey, err := service.GetPublicJWK()
|
||||
@@ -837,7 +900,10 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -868,17 +934,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
|
||||
assert.Equal(t, "rsauser456", subject, "Token subject should match user ID")
|
||||
issuer, ok := claims.Issuer()
|
||||
_ = assert.True(t, ok, "Issuer not found in token") &&
|
||||
assert.Equal(t, common.EnvConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
|
||||
// Verify the key type is RSA
|
||||
publicKey, err := service.GetPublicJWK()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, jwa.RSA().String(), publicKey.KeyType().String(), "Key type should be RSA")
|
||||
|
||||
// Verify the algorithm is RS256
|
||||
alg, ok := publicKey.Algorithm()
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, jwa.RS256().String(), alg.String(), "Algorithm should be RS256")
|
||||
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -892,16 +948,16 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
})
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
originalAppURL := common.EnvConfig.AppURL
|
||||
common.EnvConfig.AppURL = "https://test.example.com"
|
||||
defer func() {
|
||||
common.EnvConfig.AppURL = originalAppURL
|
||||
}()
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
t.Run("generates and verifies OAuth access token with standard claims", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create a test user
|
||||
@@ -931,7 +987,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
assert.Equal(t, []string{clientID}, audience, "Audience should contain the client ID")
|
||||
issuer, ok := claims.Issuer()
|
||||
_ = assert.True(t, ok, "Issuer not found in token") &&
|
||||
assert.Equal(t, common.EnvConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
|
||||
|
||||
// Check token expiration time is approximately 1 hour from now
|
||||
expectedExp := time.Now().Add(1 * time.Hour)
|
||||
@@ -944,7 +1000,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
t.Run("fails verification for expired token", func(t *testing.T) {
|
||||
// Create a JWT service with a mock function to generate an expired token
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create a test user
|
||||
@@ -961,7 +1017,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
Expiration(time.Now().Add(-1 * time.Hour)). // Expired 1 hour ago
|
||||
IssuedAt(time.Now().Add(-2 * time.Hour)).
|
||||
Audience([]string{clientID}).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(service.envConfig.AppURL).
|
||||
Build()
|
||||
require.NoError(t, err, "Failed to build token")
|
||||
|
||||
@@ -980,11 +1036,17 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
t.Run("fails verification with invalid signature", func(t *testing.T) {
|
||||
// Create two JWT services with different keys
|
||||
service1 := &JwtService{}
|
||||
err := service1.init(mockConfig, t.TempDir()) // Use a different temp dir
|
||||
err := service1.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: t.TempDir(), // Use a different temp dir
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize first JWT service")
|
||||
|
||||
service2 := &JwtService{}
|
||||
err = service2.init(mockConfig, t.TempDir()) // Use a different temp dir
|
||||
err = service2.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: t.TempDir(), // Use a different temp dir
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize second JWT service")
|
||||
|
||||
// Create a test user
|
||||
@@ -1014,7 +1076,10 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -1068,7 +1133,10 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -1122,7 +1190,10 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
|
||||
|
||||
// Create a JWT service that loads the key
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Verify it loaded the right key
|
||||
@@ -1176,16 +1247,16 @@ func TestGenerateVerifyOAuthRefreshToken(t *testing.T) {
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{})
|
||||
|
||||
// Setup the environment variable required by the token verification
|
||||
originalAppURL := common.EnvConfig.AppURL
|
||||
common.EnvConfig.AppURL = "https://test.example.com"
|
||||
defer func() {
|
||||
common.EnvConfig.AppURL = originalAppURL
|
||||
}()
|
||||
mockEnvConfig := &common.EnvConfigSchema{
|
||||
AppURL: "https://test.example.com",
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
}
|
||||
|
||||
t.Run("generates and verifies refresh token", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Create a test user
|
||||
@@ -1211,7 +1282,7 @@ func TestGenerateVerifyOAuthRefreshToken(t *testing.T) {
|
||||
t.Run("fails verification for expired token", func(t *testing.T) {
|
||||
// Create a JWT service
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, mockEnvConfig)
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
// Generate a token using JWT directly to create an expired token
|
||||
@@ -1220,7 +1291,7 @@ func TestGenerateVerifyOAuthRefreshToken(t *testing.T) {
|
||||
Expiration(time.Now().Add(-1 * time.Hour)). // Expired 1 hour ago
|
||||
IssuedAt(time.Now().Add(-2 * time.Hour)).
|
||||
Audience([]string{"client123"}).
|
||||
Issuer(common.EnvConfig.AppURL).
|
||||
Issuer(service.envConfig.AppURL).
|
||||
Build()
|
||||
require.NoError(t, err, "Failed to build token")
|
||||
|
||||
@@ -1236,11 +1307,17 @@ func TestGenerateVerifyOAuthRefreshToken(t *testing.T) {
|
||||
t.Run("fails verification with invalid signature", func(t *testing.T) {
|
||||
// Create two JWT services with different keys
|
||||
service1 := &JwtService{}
|
||||
err := service1.init(mockConfig, t.TempDir())
|
||||
err := service1.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: t.TempDir(), // Use a different temp dir
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize first JWT service")
|
||||
|
||||
service2 := &JwtService{}
|
||||
err = service2.init(mockConfig, t.TempDir())
|
||||
err = service2.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: t.TempDir(), // Use a different temp dir
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize second JWT service")
|
||||
|
||||
// Generate a token with the first service
|
||||
@@ -1308,7 +1385,10 @@ func TestGetTokenType(t *testing.T) {
|
||||
// Initialize the JWT service
|
||||
mockConfig := NewTestAppConfigService(&model.AppConfig{})
|
||||
service := &JwtService{}
|
||||
err := service.init(mockConfig, tempDir)
|
||||
err := service.init(nil, mockConfig, &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: tempDir,
|
||||
})
|
||||
require.NoError(t, err, "Failed to initialize JWT service")
|
||||
|
||||
buildTokenForType := func(t *testing.T, typ string, setClaimsFn func(b *jwt.Builder)) string {
|
||||
@@ -1402,10 +1482,19 @@ func TestGetTokenType(t *testing.T) {
|
||||
func importKey(t *testing.T, privateKeyRaw any, path string) string {
|
||||
t.Helper()
|
||||
|
||||
privateKey, err := utils.ImportRawKey(privateKeyRaw)
|
||||
privateKey, err := jwkutils.ImportRawKey(privateKeyRaw, "", "")
|
||||
require.NoError(t, err, "Failed to import private key")
|
||||
|
||||
err = SaveKeyJWK(privateKey, filepath.Join(path, PrivateKeyFile))
|
||||
keyProvider := &jwkutils.KeyProviderFile{}
|
||||
err = keyProvider.Init(jwkutils.KeyProviderOpts{
|
||||
EnvConfig: &common.EnvConfigSchema{
|
||||
KeysStorage: "file",
|
||||
KeysPath: path,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "Failed to init file key provider")
|
||||
|
||||
err = keyProvider.SaveKey(privateKey)
|
||||
require.NoError(t, err, "Failed to save key")
|
||||
|
||||
kid, _ := privateKey.KeyID()
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
||||
)
|
||||
|
||||
// generateTestECDSAKey creates an ECDSA key for testing
|
||||
@@ -62,12 +63,12 @@ func TestOidcService_jwkSetForURL(t *testing.T) {
|
||||
)
|
||||
mockResponses := map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
url1: NewMockResponse(http.StatusOK, string(jwkSetJSON1)),
|
||||
url1: testutils.NewMockResponse(http.StatusOK, string(jwkSetJSON1)),
|
||||
//nolint:bodyclose
|
||||
url2: NewMockResponse(http.StatusOK, string(jwkSetJSON2)),
|
||||
url2: testutils.NewMockResponse(http.StatusOK, string(jwkSetJSON2)),
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &MockRoundTripper{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
Responses: mockResponses,
|
||||
},
|
||||
}
|
||||
@@ -139,7 +140,7 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
|
||||
var err error
|
||||
// Create a test database
|
||||
db := newDatabaseForTest(t)
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
|
||||
// Create two JWKs for testing
|
||||
privateJWK, jwkSetJSON := generateTestECDSAKey(t)
|
||||
@@ -149,12 +150,12 @@ func TestOidcService_verifyClientCredentialsInternal(t *testing.T) {
|
||||
|
||||
// Create a mock HTTP client with custom transport to return the JWKS
|
||||
httpClient := &http.Client{
|
||||
Transport: &MockRoundTripper{
|
||||
Transport: &testutils.MockRoundTripper{
|
||||
Responses: map[string]*http.Response{
|
||||
//nolint:bodyclose
|
||||
federatedClientIssuer + "/jwks.json": NewMockResponse(http.StatusOK, string(jwkSetJSON)),
|
||||
federatedClientIssuer + "/jwks.json": testutils.NewMockResponse(http.StatusOK, string(jwkSetJSON)),
|
||||
//nolint:bodyclose
|
||||
federatedClientIssuerDefaults + ".well-known/jwks.json": NewMockResponse(http.StatusOK, string(jwkSetJSONDefaults)),
|
||||
federatedClientIssuerDefaults + ".well-known/jwks.json": testutils.NewMockResponse(http.StatusOK, string(jwkSetJSONDefaults)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
|
||||
func newDatabaseForTest(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
// Get a name for this in-memory database that is specific to the test
|
||||
dbName := utils.CreateSha256Hash(t.Name())
|
||||
|
||||
// Connect to a new in-memory SQL database
|
||||
db, err := gorm.Open(
|
||||
sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"),
|
||||
&gorm.Config{
|
||||
TranslateError: true,
|
||||
Logger: logger.New(
|
||||
testLoggerAdapter{t: t},
|
||||
logger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: logger.Info,
|
||||
IgnoreRecordNotFoundError: false,
|
||||
ParameterizedQueries: false,
|
||||
Colorful: false,
|
||||
},
|
||||
),
|
||||
})
|
||||
require.NoError(t, err, "Failed to connect to test database")
|
||||
|
||||
// Perform migrations with the embedded migrations
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err, "Failed to get sql.DB")
|
||||
driver, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{})
|
||||
require.NoError(t, err, "Failed to create migration driver")
|
||||
source, err := iofs.New(resources.FS, "migrations/sqlite")
|
||||
require.NoError(t, err, "Failed to create embedded migration source")
|
||||
m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver)
|
||||
require.NoError(t, err, "Failed to create migration instance")
|
||||
err = m.Up()
|
||||
require.NoError(t, err, "Failed to perform migrations")
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// Implements gorm's logger.Writer interface
|
||||
type testLoggerAdapter struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (l testLoggerAdapter) Printf(format string, args ...any) {
|
||||
l.t.Logf(format, args...)
|
||||
}
|
||||
|
||||
// MockRoundTripper is a custom http.RoundTripper that returns responses based on the URL
|
||||
type MockRoundTripper struct {
|
||||
Err error
|
||||
Responses map[string]*http.Response
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface
|
||||
func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Check if we have a specific response for this URL
|
||||
for url, resp := range m.Responses {
|
||||
if req.URL.String() == url {
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
return NewMockResponse(http.StatusNotFound, ""), nil
|
||||
}
|
||||
|
||||
// NewMockResponse creates an http.Response with the given status code and body
|
||||
func NewMockResponse(statusCode int, body string) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user