Improve browser caching with signed JWT photo URLs
* JWT tokens will now expire on the 10th of the next month, to produce consistent values for a period of time and aid with browser caching.
This commit is contained in:
parent
77a9d9a7fd
commit
2262edfe09
|
@ -131,7 +131,7 @@ func Landing() http.HandlerFunc {
|
||||||
Gender: Gender(currentUser),
|
Gender: Gender(currentUser),
|
||||||
VIP: isShy, // "shy accounts" use the "VIP" status for special icon in chat
|
VIP: isShy, // "shy accounts" use the "VIP" status for special icon in chat
|
||||||
Rules: rules,
|
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))
|
token, err := encryption.SignClaims(claims, []byte(config.Current.BareRTC.JWTSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,11 +14,11 @@ import (
|
||||||
// It will include values for Subject (username), Issuer (site title), ExpiresAt, IssuedAt, NotBefore.
|
// It will include values for Subject (username), Issuer (site title), ExpiresAt, IssuedAt, NotBefore.
|
||||||
//
|
//
|
||||||
// If the userID is >0, the ID field is included.
|
// 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{
|
claim := jwt.RegisteredClaims{
|
||||||
Subject: username,
|
Subject: username,
|
||||||
Issuer: config.Title,
|
Issuer: config.Title,
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expires)),
|
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/encryption"
|
"code.nonshy.com/nonshy/website/pkg/encryption"
|
||||||
"code.nonshy.com/nonshy/website/pkg/log"
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/utility"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"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.
|
// SignedPhotoURL returns a URL path to a photo's filename, signed for the current user only.
|
||||||
func SignedPhotoURL(user *models.User, filename string) string {
|
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
|
// 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,
|
// 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.
|
// and it must be viewable to all users for a long time.
|
||||||
func SignedPublicAvatarURL(filename string) string {
|
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.
|
// 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.
|
// 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{
|
// Claims expire on the 10th of next month.
|
||||||
|
var (
|
||||||
|
expiresAt = utility.NextMonth(time.Now(), 10)
|
||||||
|
claims = SignedPhotoClaims{
|
||||||
FilenameHash: FilenameHash(filename),
|
FilenameHash: FilenameHash(filename),
|
||||||
Anyone: anyone,
|
Anyone: anyone,
|
||||||
RegisteredClaims: encryption.StandardClaims(userID, username, expires),
|
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)
|
log.Debug("createSignedPhotoURL(%s): %+v", filename, claims)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,22 @@ import (
|
||||||
"time"
|
"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.
|
// FormatDurationCoarse returns a pretty printed duration with coarse granularity.
|
||||||
func FormatDurationCoarse(duration time.Duration) string {
|
func FormatDurationCoarse(duration time.Duration) string {
|
||||||
// Negative durations (e.g. future dates) should work too.
|
// Negative durations (e.g. future dates) should work too.
|
||||||
|
|
|
@ -7,6 +7,53 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/utility"
|
"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) {
|
func TestFormatDurationCoarse(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
In time.Duration
|
In time.Duration
|
||||||
|
|
Loading…
Reference in New Issue
Block a user