cbdabe791e
* 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)
107 lines
2.9 KiB
Go
107 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"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/photo"
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
|
)
|
|
|
|
// PhotoSignAuth API protects paths like /static/photos/ to authenticated user requests only.
|
|
func PhotoSignAuth() http.HandlerFunc {
|
|
type Response struct {
|
|
Success bool `json:"success"`
|
|
Error string `json:",omitempty"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
logAndError := func(w http.ResponseWriter, m string, v ...interface{}) {
|
|
log.Debug("ERROR PhotoSignAuth: "+m, v...)
|
|
SendJSON(w, http.StatusForbidden, Response{
|
|
Error: fmt.Sprintf(m, v...),
|
|
})
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// We only protect the /static/photos subpath.
|
|
// And check if the SignedPhoto feature is enabled and enforcing.
|
|
var originalURI = r.Header.Get("X-Original-URI")
|
|
if !config.Current.SignedPhoto.Enabled || !strings.HasPrefix(originalURI, config.PhotoWebPath) {
|
|
SendJSON(w, http.StatusOK, Response{
|
|
Success: true,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get the base filename.
|
|
var filename = strings.TrimPrefix(
|
|
strings.SplitN(originalURI, config.PhotoWebPath, 2)[1],
|
|
"/",
|
|
)
|
|
filename = strings.SplitN(filename, "?", 2)[0] // inner query string too
|
|
|
|
// Parse the JWT token parameter from the original URL.
|
|
var token string
|
|
if path, err := url.Parse(originalURI); err == nil {
|
|
query := path.Query()
|
|
token = query.Get("jwt")
|
|
}
|
|
|
|
// The JWT token is required from here on out.
|
|
if token == "" {
|
|
logAndError(w, "JWT token is required")
|
|
return
|
|
}
|
|
|
|
// Check if we're logged in and who the current username is.
|
|
var username string
|
|
if currentUser, err := session.CurrentUser(r); err == nil {
|
|
username = currentUser.Username
|
|
}
|
|
|
|
// Validate the JWT token is correctly signed and not expired.
|
|
claims, ok, err := encryption.ValidateClaims(
|
|
token,
|
|
[]byte(config.Current.SignedPhoto.JWTSecret),
|
|
&photo.SignedPhotoClaims{},
|
|
)
|
|
if !ok || err != nil {
|
|
logAndError(w, "When validating JWT claims: %v", err)
|
|
return
|
|
}
|
|
|
|
// Parse the claims to get data to validate this request.
|
|
c, ok := claims.(*photo.SignedPhotoClaims)
|
|
if !ok {
|
|
logAndError(w, "JWT claims were not the correct shape: %+v", claims)
|
|
return
|
|
}
|
|
|
|
// Was the signature for our username? (Skip if for Anyone)
|
|
if !c.Anyone && c.Subject != username {
|
|
logAndError(w, "That token did not belong to you")
|
|
return
|
|
}
|
|
|
|
// Is the file name correct?
|
|
hash := photo.FilenameHash(filename)
|
|
if hash != c.FilenameHash {
|
|
logAndError(w, "Filename hash mismatch: fn=%s hash=%s jwt=%s", filename, hash, c.FilenameHash)
|
|
return
|
|
}
|
|
|
|
log.Debug("PhotoSignAuth: JWT Signature OK! fn=%s u=%s anyone=%v expires=%+v", filename, c.Subject, c.Anyone, c.ExpiresAt)
|
|
|
|
SendJSON(w, http.StatusOK, Response{
|
|
Success: true,
|
|
Username: username,
|
|
})
|
|
})
|
|
}
|