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, }) }) }