diff --git a/pkg/controller/chat/chat.go b/pkg/controller/chat/chat.go index 8c54c93..828e6f3 100644 --- a/pkg/controller/chat/chat.go +++ b/pkg/controller/chat/chat.go @@ -131,7 +131,7 @@ func Landing() http.HandlerFunc { Gender: Gender(currentUser), VIP: isShy, // "shy accounts" use the "VIP" status for special icon in chat Rules: rules, - RegisteredClaims: encryption.StandardClaims(currentUser.ID, currentUser.Username, 5*time.Minute), + RegisteredClaims: encryption.StandardClaims(currentUser.ID, currentUser.Username, time.Now().Add(5*time.Minute)), } token, err := encryption.SignClaims(claims, []byte(config.Current.BareRTC.JWTSecret)) if err != nil { diff --git a/pkg/encryption/jwt.go b/pkg/encryption/jwt.go index e8d79d1..209596f 100644 --- a/pkg/encryption/jwt.go +++ b/pkg/encryption/jwt.go @@ -14,11 +14,11 @@ import ( // It will include values for Subject (username), Issuer (site title), ExpiresAt, IssuedAt, NotBefore. // // If the userID is >0, the ID field is included. -func StandardClaims(userID uint64, username string, expires time.Duration) jwt.RegisteredClaims { +func StandardClaims(userID uint64, username string, expiresAt time.Time) jwt.RegisteredClaims { claim := jwt.RegisteredClaims{ Subject: username, Issuer: config.Title, - ExpiresAt: jwt.NewNumericDate(time.Now().Add(expires)), + ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), } diff --git a/pkg/photo/photosign.go b/pkg/photo/photosign.go index dbbb6c0..9d62d8c 100644 --- a/pkg/photo/photosign.go +++ b/pkg/photo/photosign.go @@ -7,6 +7,7 @@ import ( "code.nonshy.com/nonshy/website/pkg/encryption" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" + "code.nonshy.com/nonshy/website/pkg/utility" "github.com/golang-jwt/jwt/v4" ) @@ -29,7 +30,7 @@ func VisibleAvatarURL(user, currentUser *models.User) string { // SignedPhotoURL returns a URL path to a photo's filename, signed for the current user only. func SignedPhotoURL(user *models.User, filename string) string { - return createSignedPhotoURL(user.ID, user.Username, filename, config.SignedPhotoJWTExpires, false) + return createSignedPhotoURL(user.ID, user.Username, filename, false) } // SignedPublicAvatarURL returns a signed URL for a user's public square avatar image, which has @@ -38,7 +39,7 @@ func SignedPhotoURL(user *models.User, filename string) string { // The primary use case is for the chat room: users are sent into chat with their avatar URL, // and it must be viewable to all users for a long time. func SignedPublicAvatarURL(filename string) string { - return createSignedPhotoURL(0, "@", filename, config.SignedPublicAvatarJWTExpires, true) + return createSignedPhotoURL(0, "@", filename, true) } // SignedPhotoClaims are a JWT claims object used to sign and authenticate image (direct .jpg) links. @@ -60,13 +61,21 @@ func FilenameHash(filename string) string { } // Common function to create a signed photo URL with an expiration. -func createSignedPhotoURL(userID uint64, username string, filename string, expires time.Duration, anyone bool) string { +func createSignedPhotoURL(userID uint64, username string, filename string, anyone bool) string { - claims := SignedPhotoClaims{ - FilenameHash: FilenameHash(filename), - Anyone: anyone, - RegisteredClaims: encryption.StandardClaims(userID, username, expires), - } + // Claims expire on the 10th of next month. + var ( + expiresAt = utility.NextMonth(time.Now(), 10) + claims = SignedPhotoClaims{ + FilenameHash: FilenameHash(filename), + Anyone: anyone, + RegisteredClaims: encryption.StandardClaims(userID, username, expiresAt), + } + ) + + // Lock the date stamps for a consistent JWT value for caching. + claims.IssuedAt = nil + claims.NotBefore = nil log.Debug("createSignedPhotoURL(%s): %+v", filename, claims) diff --git a/pkg/utility/time.go b/pkg/utility/time.go index f474859..c2231ce 100644 --- a/pkg/utility/time.go +++ b/pkg/utility/time.go @@ -7,6 +7,22 @@ import ( "time" ) +// NextMonth takes an input time (usually time.Now) and will return the next month on the given day. +// +// Example, NextMonth from any time in April should return e.g. May 10th. +func NextMonth(now time.Time, day int) time.Time { + var ( + year, month, _ = now.Date() + nextMonth = month + 1 + ) + if nextMonth > 12 { + nextMonth = 1 + year++ + } + + return time.Date(year, nextMonth, day, 0, 0, 0, 0, now.Location()) +} + // FormatDurationCoarse returns a pretty printed duration with coarse granularity. func FormatDurationCoarse(duration time.Duration) string { // Negative durations (e.g. future dates) should work too. diff --git a/pkg/utility/time_test.go b/pkg/utility/time_test.go index 1ea28d7..a9dfdfb 100644 --- a/pkg/utility/time_test.go +++ b/pkg/utility/time_test.go @@ -7,6 +7,53 @@ import ( "code.nonshy.com/nonshy/website/pkg/utility" ) +func TestNextMonth(t *testing.T) { + var tests = []struct { + Now string + Day int + Expect string + }{ + { + Now: "1995-08-01", + Day: 15, + Expect: "1995-09-15", + }, + { + Now: "2006-12-15", + Day: 11, + Expect: "2007-01-11", + }, + { + Now: "2006-12-01", + Day: 15, + Expect: "2007-01-15", + }, + { + Now: "2007-01-15", + Day: 29, // no leap day + Expect: "2007-03-01", + }, + { + Now: "2004-01-08", + Day: 29, // leap day + Expect: "2004-02-29", + }, + } + + for i, test := range tests { + now, err := time.Parse(time.DateOnly, test.Now) + if err != nil { + t.Errorf("Test #%d: parse error: %s", i, err) + continue + } + + actual := utility.NextMonth(now, test.Day).Format("2006-01-02") + if actual != test.Expect { + t.Errorf("Test #%d: expected %s but got %s", i, test.Expect, actual) + } + } +} + func TestFormatDurationCoarse(t *testing.T) { var tests = []struct { In time.Duration