Compare commits

...

1 Commits

4 changed files with 89 additions and 19 deletions

View File

@@ -1,6 +1,11 @@
package hwsauth package hwsauth
import ( import (
"context"
"database/sql"
"os"
"time"
"git.haelnorr.com/h/golib/hlog" "git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/jwt" "git.haelnorr.com/h/golib/jwt"
@@ -30,6 +35,7 @@ func NewAuthenticator[T Model, TX DBTransaction](
beginTx BeginTX, beginTx BeginTX,
logger *hlog.Logger, logger *hlog.Logger,
errorPage hws.ErrorPageFunc, errorPage hws.ErrorPageFunc,
db *sql.DB,
) (*Authenticator[T, TX], error) { ) (*Authenticator[T, TX], error) {
if load == nil { if load == nil {
return nil, errors.New("No function to load model supplied") return nil, errors.New("No function to load model supplied")
@@ -55,7 +61,10 @@ func NewAuthenticator[T Model, TX DBTransaction](
return nil, errors.New("SecretKey is required") return nil, errors.New("SecretKey is required")
} }
if cfg.SSL && cfg.TrustedHost == "" { if cfg.SSL && cfg.TrustedHost == "" {
return nil, errors.New("TrustedHost is required when SSL is enabled") cfg.SSL = false // Disable SSL if TrustedHost is not configured
}
if cfg.TrustedHost == "" {
cfg.TrustedHost = "localhost" // Default TrustedHost for JWT
} }
if cfg.AccessTokenExpiry == 0 { if cfg.AccessTokenExpiry == 0 {
cfg.AccessTokenExpiry = 5 cfg.AccessTokenExpiry = 5
@@ -69,12 +78,35 @@ func NewAuthenticator[T Model, TX DBTransaction](
if cfg.LandingPage == "" { if cfg.LandingPage == "" {
cfg.LandingPage = "/profile" cfg.LandingPage = "/profile"
} }
if cfg.DatabaseType == "" {
cfg.DatabaseType = "postgres"
}
if cfg.DatabaseVersion == "" {
cfg.DatabaseVersion = "15"
}
if db == nil {
return nil, errors.New("No Database provided")
}
// Test database connectivity
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, errors.Wrap(err, "database connection test failed")
}
// Configure JWT table // Configure JWT table
tableConfig := jwt.DefaultTableConfig() tableConfig := jwt.DefaultTableConfig()
if cfg.JWTTableName != "" { if cfg.JWTTableName != "" {
tableConfig.TableName = cfg.JWTTableName tableConfig.TableName = cfg.JWTTableName
} }
// Disable auto-creation for tests
// Check for test environment or mock database
if os.Getenv("GO_TEST") == "1" {
tableConfig.AutoCreate = false
tableConfig.EnableAutoCleanup = false
}
// Create token generator // Create token generator
tokenGen, err := jwt.CreateGenerator(jwt.GeneratorConfig{ tokenGen, err := jwt.CreateGenerator(jwt.GeneratorConfig{
@@ -87,6 +119,7 @@ func NewAuthenticator[T Model, TX DBTransaction](
Type: cfg.DatabaseType, Type: cfg.DatabaseType,
Version: cfg.DatabaseVersion, Version: cfg.DatabaseVersion,
}, },
DB: db,
TableConfig: tableConfig, TableConfig: tableConfig,
}, beginTx) }, beginTx)
if err != nil { if err != nil {

View File

@@ -8,6 +8,7 @@ require (
git.haelnorr.com/h/golib/hlog v0.10.4 git.haelnorr.com/h/golib/hlog v0.10.4
git.haelnorr.com/h/golib/hws v0.3.0 git.haelnorr.com/h/golib/hws v0.3.0
git.haelnorr.com/h/golib/jwt v0.10.1 git.haelnorr.com/h/golib/jwt v0.10.1
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
) )

View File

@@ -2,16 +2,10 @@ git.haelnorr.com/h/golib/cookies v0.9.0 h1:Vf+eX1prHkKuGrQon1BHY87yaPc1H+HJFRXDO
git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo= git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo=
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY= git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg= git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
git.haelnorr.com/h/golib/hlog v0.9.1 h1:9VmE/IQTfD8LAEyTbUCZLy/+8PbcHA1Kob/WQHRHKzc=
git.haelnorr.com/h/golib/hlog v0.9.1/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
git.haelnorr.com/h/golib/hlog v0.10.4 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ= git.haelnorr.com/h/golib/hlog v0.10.4 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ=
git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc= git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc=
git.haelnorr.com/h/golib/hws v0.2.0 h1:MR2Tu2qPaW+/oK8aXFJLRFaYZIHgKiex3t3zE41cu1U=
git.haelnorr.com/h/golib/hws v0.2.0/go.mod h1:6ZlRKnt8YMpv5XcMXmyBGmD1/euvBo3d1azEvHJjOLo=
git.haelnorr.com/h/golib/hws v0.3.0 h1:/YGzxd3sRR3DFU6qVZxpJMKV3W2wCONqZKYUDIercCo= git.haelnorr.com/h/golib/hws v0.3.0 h1:/YGzxd3sRR3DFU6qVZxpJMKV3W2wCONqZKYUDIercCo=
git.haelnorr.com/h/golib/hws v0.3.0/go.mod h1:6ZlRKnt8YMpv5XcMXmyBGmD1/euvBo3d1azEvHJjOLo= git.haelnorr.com/h/golib/hws v0.3.0/go.mod h1:6ZlRKnt8YMpv5XcMXmyBGmD1/euvBo3d1azEvHJjOLo=
git.haelnorr.com/h/golib/jwt v0.10.0 h1:8cI8mSnb8X+EmJtrBO/5UZwuBMtib0IE9dv85gkm94E=
git.haelnorr.com/h/golib/jwt v0.10.0/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI= git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
git.haelnorr.com/h/golib/jwt v0.10.1/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4= git.haelnorr.com/h/golib/jwt v0.10.1/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
@@ -26,6 +20,7 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=

View File

@@ -10,6 +10,7 @@ import (
"git.haelnorr.com/h/golib/hlog" "git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/golib/hws"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -47,6 +48,28 @@ func (tep TestErrorPage) Render(ctx context.Context, w io.Writer) error {
return nil return nil
} }
// createMockDB creates a mock SQL database for testing
func createMockDB() (*sql.DB, sqlmock.Sqlmock, error) {
db, mock, err := sqlmock.New()
if err != nil {
return nil, nil, err
}
// Expect a ping to succeed for database connectivity test
mock.ExpectPing()
// Expect table existence check (returns a row = table exists)
mock.ExpectQuery(`SELECT 1 FROM information_schema\.tables WHERE table_schema = 'public' AND table_name = \$1`).
WithArgs("jwtblacklist").
WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
// Expect cleanup function creation
mock.ExpectExec(`CREATE OR REPLACE FUNCTION cleanup_jwtblacklist\(\) RETURNS void AS \$\$ BEGIN DELETE FROM jwtblacklist WHERE exp < EXTRACT\(EPOCH FROM NOW\(\)\); END; \$\$ LANGUAGE plpgsql;`).
WillReturnResult(sqlmock.NewResult(0, 0))
return db, mock, nil
}
func TestGetNil(t *testing.T) { func TestGetNil(t *testing.T) {
var zero TestModel var zero TestModel
result := getNil[TestModel]() result := getNil[TestModel]()
@@ -209,12 +232,13 @@ func TestNewAuthenticator_NilConfig(t *testing.T) {
} }
auth, err := NewAuthenticator( auth, err := NewAuthenticator(
nil, nil, // cfg
load, load,
server, server,
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
nil, // db
) )
assert.Error(t, err) assert.Error(t, err)
@@ -246,6 +270,7 @@ func TestNewAuthenticator_MissingSecretKey(t *testing.T) {
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
nil, // db - will fail before db check since SecretKey is missing
) )
assert.Error(t, err) assert.Error(t, err)
@@ -274,6 +299,7 @@ func TestNewAuthenticator_NilLoadFunction(t *testing.T) {
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
nil, // db
) )
assert.Error(t, err) assert.Error(t, err)
@@ -299,6 +325,10 @@ func TestNewAuthenticator_SSLWithoutTrustedHost(t *testing.T) {
return TestErrorPage{}, nil return TestErrorPage{}, nil
} }
db, _, err := createMockDB()
require.NoError(t, err)
defer db.Close()
auth, err := NewAuthenticator( auth, err := NewAuthenticator(
cfg, cfg,
load, load,
@@ -306,17 +336,19 @@ func TestNewAuthenticator_SSLWithoutTrustedHost(t *testing.T) {
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
db,
) )
assert.Error(t, err) require.NoError(t, err)
assert.Nil(t, auth) require.NotNil(t, auth)
assert.Contains(t, err.Error(), "TrustedHost is required when SSL is enabled")
assert.Equal(t, false, auth.SSL)
assert.Equal(t, "/profile", auth.LandingPage)
} }
func TestNewAuthenticator_ValidMinimalConfig(t *testing.T) { func TestNewAuthenticator_NilDatabase(t *testing.T) {
cfg := &Config{ cfg := &Config{
SecretKey: "test-secret", SecretKey: "test-secret",
TrustedHost: "example.com",
} }
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) { load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
@@ -338,13 +370,12 @@ func TestNewAuthenticator_ValidMinimalConfig(t *testing.T) {
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
nil, // db
) )
require.NoError(t, err) assert.Error(t, err)
require.NotNil(t, auth) assert.Nil(t, auth)
assert.Contains(t, err.Error(), "No Database provided")
assert.Equal(t, false, auth.SSL)
assert.Equal(t, "/profile", auth.LandingPage)
} }
func TestModelInterface(t *testing.T) { func TestModelInterface(t *testing.T) {
@@ -376,6 +407,10 @@ func TestGetAuthenticatedUser_NoTokens(t *testing.T) {
return TestErrorPage{}, nil return TestErrorPage{}, nil
} }
db, _, err := createMockDB()
require.NoError(t, err)
defer db.Close()
auth, err := NewAuthenticator( auth, err := NewAuthenticator(
cfg, cfg,
load, load,
@@ -383,6 +418,7 @@ func TestGetAuthenticatedUser_NoTokens(t *testing.T) {
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
db,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -416,6 +452,10 @@ func TestLogin_BasicFunctionality(t *testing.T) {
return TestErrorPage{}, nil return TestErrorPage{}, nil
} }
db, _, err := createMockDB()
require.NoError(t, err)
defer db.Close()
auth, err := NewAuthenticator( auth, err := NewAuthenticator(
cfg, cfg,
load, load,
@@ -423,6 +463,7 @@ func TestLogin_BasicFunctionality(t *testing.T) {
beginTx, beginTx,
logger, logger,
errorPage, errorPage,
db,
) )
require.NoError(t, err) require.NoError(t, err)