7869ff83ba
* Add support for authenticated static photo URLs, leveraging the NGINX module ngx_http_auth_request. The README is updated with an example NGINX config how to set this up on the proxy side. * In settings.json a new SignedPhoto section is added: not enabled by default. * PhotoURL will append a ?jwt= token to the /static/photos/ path for the current user, which expires after 30 seconds. * When SignedPhoto is enabled, it will enforce that the JWT token is valid and matches the username of the current logged-in user, or else will return with a 403 Forbidden error.
85 lines
2.1 KiB
Go
85 lines
2.1 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/session"
|
|
"github.com/golang-jwt/jwt/v4"
|
|
)
|
|
|
|
// StaticAuth API protects paths like /static/photos/ to authenticated user requests only.
|
|
func StaticAuth() http.HandlerFunc {
|
|
type Response struct {
|
|
Success bool `json:"success"`
|
|
Error string `json:",omitempty"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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 token is required from here.
|
|
if token == "" {
|
|
SendJSON(w, http.StatusForbidden, Response{
|
|
Error: "JWT token is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if we're logged in.
|
|
currentUser, err := session.CurrentUser(r)
|
|
if err != nil {
|
|
SendJSON(w, http.StatusForbidden, Response{
|
|
Error: "Login Required",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate the JWT token.
|
|
claims, ok, err := encryption.ValidateClaims(
|
|
token,
|
|
[]byte(config.Current.SignedPhoto.JWTSecret),
|
|
&jwt.RegisteredClaims{},
|
|
)
|
|
if !ok || err != nil {
|
|
SendJSON(w, http.StatusForbidden, Response{
|
|
Error: fmt.Sprintf("JWT claims: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Sanity check that the username in these claims is the same as the viewer.
|
|
if c, ok := claims.(*jwt.RegisteredClaims); !ok || c.Subject != currentUser.Username {
|
|
SendJSON(w, http.StatusForbidden, Response{
|
|
Error: "That photo was not for you",
|
|
})
|
|
return
|
|
}
|
|
|
|
SendJSON(w, http.StatusOK, Response{
|
|
Success: true,
|
|
Username: currentUser.Username,
|
|
})
|
|
})
|
|
}
|