- Refactory cookie and API session handle. Insolated y routes and controllers (api-auth.go, app-auth.go).
- Cookie lifetime with sliding expiration - Fix bug single global expiresAt in JWT, now each token has unique ExpiresAt
This commit is contained in:
parent
ab7c00575c
commit
0e20a17ce9
5 changed files with 241 additions and 86 deletions
27
cookies.go
27
cookies.go
|
|
@ -86,7 +86,24 @@ func GetCookie(r *http.Request) (UserCookie, error) {
|
||||||
|
|
||||||
// SetCookie sets an encrypted cookie with a user's email and token, using gob encoding for data serialization.
|
// SetCookie sets an encrypted cookie with a user's email and token, using gob encoding for data serialization.
|
||||||
// The Secure flag is controlled by the COOKIE_SECURE environment variable (defaults to true, set to false for local HTTP development).
|
// The Secure flag is controlled by the COOKIE_SECURE environment variable (defaults to true, set to false for local HTTP development).
|
||||||
|
// The cookie lifetime is derived from JWT_LIFESPAN_MINUTES.
|
||||||
func SetCookie(w http.ResponseWriter, email string, token string) error {
|
func SetCookie(w http.ResponseWriter, email string, token string) error {
|
||||||
|
// Derive cookie MaxAge from JWT_LIFESPAN_MINUTES (default: 1440 min = 1 day)
|
||||||
|
maxAge := 1440 * 60 // default 1 day in seconds
|
||||||
|
lifetimeStr := os.Getenv("JWT_LIFESPAN_MINUTES")
|
||||||
|
if lifetimeStr != "" {
|
||||||
|
lifetime, parseErr := strconv.Atoi(lifetimeStr)
|
||||||
|
if parseErr == nil {
|
||||||
|
maxAge = lifetime * 60 // convert minutes to seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SetCookieWithMaxAge(w, email, token, maxAge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCookieWithMaxAge sets the encrypted "goffee" cookie with an explicit lifetime in seconds.
|
||||||
|
// It is used for sliding session renewal, where the cookie expiration must be refreshed
|
||||||
|
// to a full lifetime from the moment of renewal.
|
||||||
|
func SetCookieWithMaxAge(w http.ResponseWriter, email string, token string, maxAge int) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// check if template engine is enable
|
// check if template engine is enable
|
||||||
|
|
@ -124,16 +141,6 @@ func SetCookie(w http.ResponseWriter, email string, token string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive cookie MaxAge from JWT_LIFESPAN_MINUTES (default: 1440 min = 1 day)
|
|
||||||
maxAge := 1440 * 60 // default 1 day in seconds
|
|
||||||
lifetimeStr := os.Getenv("JWT_LIFESPAN_MINUTES")
|
|
||||||
if lifetimeStr != "" {
|
|
||||||
lifetime, parseErr := strconv.Atoi(lifetimeStr)
|
|
||||||
if parseErr == nil {
|
|
||||||
maxAge = lifetime * 60 // convert minutes to seconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if the cookie should have the Secure flag.
|
// Determine if the cookie should have the Secure flag.
|
||||||
// Set COOKIE_SECURE=false (or "0", "f") in your .env for local development over HTTP.
|
// Set COOKIE_SECURE=false (or "0", "f") in your .env for local development over HTTP.
|
||||||
// Defaults to true for production safety.
|
// Defaults to true for production safety.
|
||||||
|
|
|
||||||
32
go.mod
32
go.mod
|
|
@ -4,7 +4,7 @@ replace git.smarteching.com/goffee/core/logger => ./logger
|
||||||
|
|
||||||
replace git.smarteching.com/goffee/core/env => ./env
|
replace git.smarteching.com/goffee/core/env => ./env
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.smarteching.com/zeni/go-chart/v2 v2.1.4
|
git.smarteching.com/zeni/go-chart/v2 v2.1.4
|
||||||
|
|
@ -16,13 +16,13 @@ require (
|
||||||
github.com/hibiken/asynq v0.26.0
|
github.com/hibiken/asynq v0.26.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/redis/go-redis/v9 v9.21.0
|
github.com/redis/go-redis/v9 v9.22.0
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/text v0.38.0
|
golang.org/x/text v0.42.0
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.2
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -30,17 +30,17 @@ require (
|
||||||
github.com/SparkPost/gosparkpost v0.2.0 // indirect
|
github.com/SparkPost/gosparkpost v0.2.0 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.3.0 // indirect
|
github.com/go-chi/chi/v5 v5.3.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.10.0 // indirect
|
github.com/go-sql-driver/mysql v1.10.1 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.10.0 // indirect
|
github.com/jackc/pgx/v5 v5.11.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/mailgun/errors v0.6.0 // indirect
|
github.com/mailgun/errors v0.6.0 // indirect
|
||||||
github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect
|
github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.47 // indirect
|
github.com/mattn/go-sqlite3 v1.14.52 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
|
@ -49,12 +49,12 @@ require (
|
||||||
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
|
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/image v0.43.0 // indirect
|
golang.org/x/image v0.46.0 // indirect
|
||||||
golang.org/x/net v0.56.0 // indirect
|
golang.org/x/net v0.59.0 // indirect
|
||||||
golang.org/x/sync v0.21.0 // indirect
|
golang.org/x/sync v0.23.0 // indirect
|
||||||
golang.org/x/sys v0.46.0 // indirect
|
golang.org/x/sys v0.48.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.16.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.12 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
||||||
82
go.sum
82
go.sum
|
|
@ -25,14 +25,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||||
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
|
||||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
|
||||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
|
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
|
||||||
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
|
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
|
||||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME=
|
||||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
|
@ -52,10 +50,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
|
||||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||||
|
|
@ -78,17 +74,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/mailgun/errors v0.5.0 h1:pLQo8uhAdORsjN69mGixSr0pGs46z/BW/FQXd8HG1VM=
|
|
||||||
github.com/mailgun/errors v0.5.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs=
|
|
||||||
github.com/mailgun/errors v0.6.0 h1:IWmzIGwXCSN/Q60JT/lXvam3xRAgTUJSX88KwKJ7hss=
|
github.com/mailgun/errors v0.6.0 h1:IWmzIGwXCSN/Q60JT/lXvam3xRAgTUJSX88KwKJ7hss=
|
||||||
github.com/mailgun/errors v0.6.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs=
|
github.com/mailgun/errors v0.6.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs=
|
||||||
github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
|
github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
|
||||||
github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
|
github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|
@ -100,10 +92,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
|
||||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
|
||||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
|
||||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
|
@ -130,48 +120,36 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
|
||||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
|
||||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
|
||||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
||||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
|
||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.2 h1:BvXQ/cNUg63q5TFNg672DmDcowZSFrNLkkA3Xe6GXq4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.2/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
|
|
||||||
57
jwt.go
57
jwt.go
|
|
@ -9,8 +9,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type JWT struct {
|
type JWT struct {
|
||||||
signingKey []byte
|
signingKey []byte
|
||||||
expiresAt time.Time
|
lifetimeMinutes int
|
||||||
}
|
}
|
||||||
type JWTOptions struct {
|
type JWTOptions struct {
|
||||||
SigningKey string
|
SigningKey string
|
||||||
|
|
@ -20,10 +20,9 @@ type JWTOptions struct {
|
||||||
var j *JWT
|
var j *JWT
|
||||||
|
|
||||||
func newJWT(opts JWTOptions) *JWT {
|
func newJWT(opts JWTOptions) *JWT {
|
||||||
d := time.Duration(opts.LifetimeMinutes)
|
|
||||||
j = &JWT{
|
j = &JWT{
|
||||||
signingKey: []byte(opts.SigningKey),
|
signingKey: []byte(opts.SigningKey),
|
||||||
expiresAt: time.Now().Add(d * time.Minute),
|
lifetimeMinutes: opts.LifetimeMinutes,
|
||||||
}
|
}
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
@ -31,13 +30,19 @@ func resolveJWT() *JWT {
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LifetimeMinutes returns the configured token lifetime in minutes.
|
||||||
|
func (j *JWT) LifetimeMinutes() int {
|
||||||
|
return j.lifetimeMinutes
|
||||||
|
}
|
||||||
|
|
||||||
type claims struct {
|
type claims struct {
|
||||||
J []byte
|
J []byte
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JWT) GenerateToken(payload map[string]interface{}) (string, error) {
|
func (j *JWT) GenerateToken(payload map[string]interface{}) (string, error) {
|
||||||
claims, err := mapClaims(payload, j.expiresAt)
|
expiresAt := time.Now().Add(time.Duration(j.lifetimeMinutes) * time.Minute)
|
||||||
|
claims, err := mapClaims(payload, expiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +54,46 @@ func (j *JWT) GenerateToken(payload map[string]interface{}) (string, error) {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecodeTokenIgnoreExpiry decodes a token's payload without validating its expiration.
|
||||||
|
// It is used to inspect tokens that may already be expired (e.g. sliding session renewal),
|
||||||
|
// while still verifying the signature and the token structure.
|
||||||
|
func (j *JWT) DecodeTokenIgnoreExpiry(token string) (payload map[string]interface{}, err error) {
|
||||||
|
t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return j.signingKey, nil
|
||||||
|
}, jwt.WithoutClaimsValidation())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, ok := t.Claims.(*claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("error decoding token")
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(c.J, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtIgnoreExpiry returns the expiration time carried by the token, without
|
||||||
|
// validating it. It verifies the signature and returns the token's "exp" claim.
|
||||||
|
func (j *JWT) ExpiresAtIgnoreExpiry(token string) (time.Time, error) {
|
||||||
|
t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return j.signingKey, nil
|
||||||
|
}, jwt.WithoutClaimsValidation())
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
c, ok := t.Claims.(*claims)
|
||||||
|
if !ok {
|
||||||
|
return time.Time{}, errors.New("error decoding token")
|
||||||
|
}
|
||||||
|
if c.ExpiresAt == nil {
|
||||||
|
return time.Time{}, errors.New("token has no expiration")
|
||||||
|
}
|
||||||
|
return time.Unix(c.ExpiresAt.Unix(), 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *JWT) DecodeToken(token string) (payload map[string]interface{}, err error) {
|
func (j *JWT) DecodeToken(token string) (payload map[string]interface{}, err error) {
|
||||||
t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return j.signingKey, nil
|
return j.signingKey, nil
|
||||||
|
|
|
||||||
129
session.go
129
session.go
|
|
@ -5,9 +5,12 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -37,7 +40,16 @@ func (s *SessionUser) Init(c *Context) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := c.GetJWT().DecodeToken(usercookie.Token)
|
// The request cookie is immutable, so if the session was already renewed earlier
|
||||||
|
// in this same request, the request still carries the previous token. Use the
|
||||||
|
// token that was renewed during this request (if any) so multiple Init calls in
|
||||||
|
// the same request remain consistent.
|
||||||
|
currentToken := usercookie.Token
|
||||||
|
if renewed := renewedTokenForRequest(c); renewed != "" {
|
||||||
|
currentToken = renewed
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := c.GetJWT().DecodeToken(currentToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +60,7 @@ func (s *SessionUser) Init(c *Context) bool {
|
||||||
// verify token against cached value
|
// verify token against cached value
|
||||||
hashedCacheKey := CreateAuthTokenHashedCacheKey(userID, userAgent)
|
hashedCacheKey := CreateAuthTokenHashedCacheKey(userID, userAgent)
|
||||||
cachedToken, err := c.GetCache().Get(hashedCacheKey)
|
cachedToken, err := c.GetCache().Get(hashedCacheKey)
|
||||||
if err != nil || cachedToken != usercookie.Token {
|
if err != nil || cachedToken != currentToken {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,9 +77,111 @@ func (s *SessionUser) Init(c *Context) bool {
|
||||||
_ = json.Unmarshal([]byte(value), &s.values)
|
_ = json.Unmarshal([]byte(value), &s.values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sliding expiration: transparently renew the cookie + JWT if enough time elapsed
|
||||||
|
s.maybeRenew(c, usercookie.Email, userID, hashedCacheKey, currentToken)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// slidingRenewedTokenContextKey is the request-context key under which the token
|
||||||
|
// renewed during the current request is stored. This keeps the sliding session
|
||||||
|
// logic consistent if Init is called more than once within the same request.
|
||||||
|
const slidingRenewedTokenContextKey = "goffeeSlidingRenewedToken"
|
||||||
|
|
||||||
|
// renewedTokenForRequest returns the token that was issued by sliding renewal during
|
||||||
|
// the current request, or an empty string if no renewal happened yet.
|
||||||
|
func renewedTokenForRequest(c *Context) string {
|
||||||
|
if c.Request == nil || c.Request.httpRequest == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if token, ok := c.Request.httpRequest.Context().Value(slidingRenewedTokenContextKey).(string); ok {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// markRenewedTokenForRequest stores the token issued by sliding renewal in the request
|
||||||
|
// context so subsequent Init calls within the same request can use it.
|
||||||
|
func markRenewedTokenForRequest(c *Context, token string) {
|
||||||
|
if c.Request == nil || c.Request.httpRequest == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(c.Request.httpRequest.Context(), slidingRenewedTokenContextKey, token)
|
||||||
|
*c.Request.httpRequest = *c.Request.httpRequest.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// slidingRenewThresholdPercent is the fraction of the token lifetime that must
|
||||||
|
// elapse before a web session cookie is renewed (sliding expiration).
|
||||||
|
// Renewing only past this percentage keeps the renewal "moderate" — avoiding a
|
||||||
|
// cookie/JWT write on every single request.
|
||||||
|
const slidingRenewThresholdPercent = 0.25
|
||||||
|
|
||||||
|
// maybeRenew performs the sliding expiration renewal for cookie/template based sessions.
|
||||||
|
// It reads the token expiration without validating it (at this point the token has already
|
||||||
|
// been verified as valid and matched against the cache), derives the issuance moment and,
|
||||||
|
// if more than slidingRenewThresholdPercent of the lifetime has elapsed, it issues a brand
|
||||||
|
// new JWT (with a fresh expiration) and rewrites both the cookie and the cached token.
|
||||||
|
//
|
||||||
|
// Renewal errors are logged and ignored: a renewal failure must never break the current
|
||||||
|
// request nor invalidate an otherwise valid session.
|
||||||
|
func (s *SessionUser) maybeRenew(c *Context, email string, userID uint, hashedCacheKey string, currentToken string) {
|
||||||
|
// Sliding renewal only applies to cookie/template based sessions.
|
||||||
|
// Without templates there is no cookie session to slide.
|
||||||
|
if !templateEngineEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtObj := c.GetJWT()
|
||||||
|
lifetimeMinutes := jwtObj.LifetimeMinutes()
|
||||||
|
if lifetimeMinutes <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the token expiration without validating it. The token may be close to
|
||||||
|
// expiring but is still valid (it was decoded successfully above).
|
||||||
|
expiresAt, err := jwtObj.ExpiresAtIgnoreExpiry(currentToken)
|
||||||
|
if err != nil {
|
||||||
|
c.GetLogger().Error(fmt.Sprintf("sliding session: error reading token expiration: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokens are issued with exp = issuedAt + lifetime, so the issuance moment can
|
||||||
|
// be derived from the expiration carried by the token.
|
||||||
|
issuedAt := expiresAt.Add(-time.Duration(lifetimeMinutes) * time.Minute)
|
||||||
|
elapsed := time.Since(issuedAt)
|
||||||
|
threshold := time.Duration(float64(lifetimeMinutes)*slidingRenewThresholdPercent) * time.Minute
|
||||||
|
|
||||||
|
if elapsed < threshold {
|
||||||
|
// Not enough time has elapsed yet — skip renewal to keep it moderate.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue a fresh JWT and refresh the cached token.
|
||||||
|
newToken, err := jwtObj.GenerateToken(map[string]interface{}{
|
||||||
|
"userID": userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.GetLogger().Error(fmt.Sprintf("sliding session: error generating new token: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.GetCache().Set(hashedCacheKey, newToken); err != nil {
|
||||||
|
c.GetLogger().Error(fmt.Sprintf("sliding session: error caching renewed token: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the cookie with a full new lifetime.
|
||||||
|
maxAgeSeconds := lifetimeMinutes * 60
|
||||||
|
if err := SetCookieWithMaxAge(c.Response.HttpResponseWriter, email, newToken, maxAgeSeconds); err != nil {
|
||||||
|
c.GetLogger().Error(fmt.Sprintf("sliding session: error writing renewed cookie: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember the renewed token for the rest of this request so that any further
|
||||||
|
// Init call in the same request validates against the renewed token.
|
||||||
|
markRenewedTokenForRequest(c, newToken)
|
||||||
|
}
|
||||||
|
|
||||||
// Set stores a value in the session and persists it to the cache.
|
// Set stores a value in the session and persists it to the cache.
|
||||||
func (s *SessionUser) Set(key string, value interface{}) error {
|
func (s *SessionUser) Set(key string, value interface{}) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|
@ -144,3 +258,14 @@ func CreateAuthTokenHashedCacheKey(userID uint, userAgent string) string {
|
||||||
cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent)
|
cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent)
|
||||||
return fmt.Sprintf("%x", md5.Sum([]byte(cacheKey)))
|
return fmt.Sprintf("%x", md5.Sum([]byte(cacheKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// templateEngineEnabled reports whether the template (cookie based) engine is enabled.
|
||||||
|
// Sliding session renewal only applies to cookie/template based sessions.
|
||||||
|
func templateEngineEnabled() bool {
|
||||||
|
templateEnableStr := os.Getenv("TEMPLATE_ENABLE")
|
||||||
|
if templateEnableStr == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
enabled, _ := strconv.ParseBool(templateEnableStr)
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue