website/pkg/photo/photosign.go
Noah Petherbridge cbdabe791e Improve Signed Photo URLs
* The photo signing JWT tokens carry more fields to validate against:
  * The username the token is assigned to (or '@' for anyone)
  * An 'anyone' boolean for widely public images, such as for the chat room
    and public profile pages.
  * A short filename hash of the image in question (whether a Photo or a
    CommentPhoto) - so that the user can't borrow a JWT token from the chat
    room and reveal a different picture.
* Refactored where the VisibleAvatarURL function lives, to avoid a cyclic
  dependency error.
  * Originally: (*models.User).VisibleAvatarURL(other *models.User)
  * Now: (pkg/photo).VisibleAvatarURL(user, currentUser *models.User)
2024-10-03 20:14:34 -07:00

85 lines
2.9 KiB
Go

package photo
import (
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/encryption"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models"
"github.com/golang-jwt/jwt/v4"
)
// VisibleAvatarURL returns the visible URL image to a user's square profile picture, from the point of view of the currentUser.
func VisibleAvatarURL(user, currentUser *models.User) string {
canSee, visibility := user.CanSeeProfilePicture(currentUser)
if canSee {
return SignedPublicAvatarURL(user.ProfilePhoto.CroppedFilename)
}
switch visibility {
case models.PhotoPrivate:
return "/static/img/shy-private.png"
case models.PhotoFriends:
return "/static/img/shy-friends.png"
}
return "/static/img/shy.png"
}
// 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)
}
// SignedPublicAvatarURL returns a signed URL for a user's public square avatar image, which has
// a much more generous JWT expiration lifetime on it.
//
// 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)
}
// SignedPhotoClaims are a JWT claims object used to sign and authenticate image (direct .jpg) links.
type SignedPhotoClaims struct {
FilenameHash string `json:"f"` // Short hash of the Filename being signed.
Anyone bool `json:"a,omitempty"` // Non-authenticated signature (e.g. public sq avatar URLs)
// Standard claims. Notes:
// .Subject = username
jwt.RegisteredClaims
}
// FilenameHash returns a 'short' hash of the filename, for encoding in the SignedPhotoClaims.
//
// The hash is a truncated SHA256 hash as a basic validation measure against one JWT token being
// used to reveal an unrelated picture.
func FilenameHash(filename string) string {
return encryption.Hash([]byte(filename))[:6]
}
// 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 {
claims := SignedPhotoClaims{
FilenameHash: FilenameHash(filename),
Anyone: anyone,
RegisteredClaims: encryption.StandardClaims(userID, username, expires),
}
log.Debug("createSignedPhotoURL(%s): %+v", filename, claims)
token, err := encryption.SignClaims(claims, []byte(config.Current.SignedPhoto.JWTSecret))
if err != nil {
log.Error("PhotoURL: SignClaims: %s", err)
}
// JWT query string to append?
if token != "" {
token = "?jwt=" + token
}
return URLPath(filename) + token
}