mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 17:30:42 +01:00
A negative expires_after_seconds reached GenJwtForFilerServer and produced a token with no expiration claim. Also synchronize the Authorization-header capture in the proxy test and restore the prior viper key on cleanup. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
210 lines
7.0 KiB
Go
210 lines
7.0 KiB
Go
package security
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type EncodedJwt string
|
|
type SigningKey []byte
|
|
|
|
// BearerPrefix is the RFC 6750 Authorization header scheme prefix for bearer
|
|
// tokens. Used when constructing "Authorization: Bearer <token>" headers; the
|
|
// scheme name itself is matched case-insensitively when parsing (see GetJwt).
|
|
const BearerPrefix = "Bearer "
|
|
|
|
// SeaweedFileIdClaims is created by Master server(s) and consumed by Volume server(s),
|
|
// restricting the access this JWT allows to only a single file.
|
|
type SeaweedFileIdClaims struct {
|
|
Fid string `json:"fid"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// SeaweedFilerClaims is created e.g. by S3 proxy server and consumed by Filer server.
|
|
// Right now, it only contains the standard claims; but this might be extended later
|
|
// for more fine-grained permissions.
|
|
type SeaweedFilerClaims struct {
|
|
AllowedPrefixes []string `json:"allowed_prefixes,omitempty"`
|
|
AllowedMethods []string `json:"allowed_methods,omitempty"`
|
|
// SessionId is present only on STS session tokens; a token carrying it is
|
|
// an S3 session credential, not a filer API credential.
|
|
SessionId string `json:"sid,omitempty"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// SeaweedFilerAdminClaims is presented by callers of the filer's IAM gRPC
|
|
// service to prove they are authorised to administer users, access keys, and
|
|
// policies. The token is signed with the filer write-signing key
|
|
// (jwt.filer_signing.key in security.toml).
|
|
//
|
|
// Validation is delegated to DecodeJwt below: it enforces HS256 via the
|
|
// SigningMethodHMAC type check, and jwt/v5 validates exp/nbf on the embedded
|
|
// RegisteredClaims. Extra JSON fields in the payload are silently ignored by
|
|
// encoding/json, which is the desired behaviour here (forward-compat).
|
|
type SeaweedFilerAdminClaims struct {
|
|
// SessionId is present only on STS session tokens; a token carrying it is
|
|
// an S3 session credential, not a filer admin credential.
|
|
SessionId string `json:"sid,omitempty"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// GenJwtForFilerAdmin mints a Bearer token for the filer IAM gRPC service.
|
|
// Returns an empty string if the signing key is not configured.
|
|
func GenJwtForFilerAdmin(signingKey SigningKey, expiresAfterSec int) EncodedJwt {
|
|
if len(signingKey) == 0 {
|
|
return ""
|
|
}
|
|
|
|
claims := SeaweedFilerAdminClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{},
|
|
}
|
|
if expiresAfterSec > 0 {
|
|
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(expiresAfterSec)))
|
|
}
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
encoded, e := t.SignedString([]byte(signingKey))
|
|
if e != nil {
|
|
glog.V(0).Infof("Failed to sign claims %+v: %v", t.Claims, e)
|
|
return ""
|
|
}
|
|
return EncodedJwt(encoded)
|
|
}
|
|
|
|
func GenJwtForVolumeServer(signingKey SigningKey, expiresAfterSec int, fileId string) EncodedJwt {
|
|
if len(signingKey) == 0 {
|
|
return ""
|
|
}
|
|
|
|
claims := SeaweedFileIdClaims{
|
|
fileId,
|
|
jwt.RegisteredClaims{},
|
|
}
|
|
if expiresAfterSec > 0 {
|
|
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(expiresAfterSec)))
|
|
}
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
encoded, e := t.SignedString([]byte(signingKey))
|
|
if e != nil {
|
|
glog.V(0).Infof("Failed to sign claims %+v: %v", t.Claims, e)
|
|
return ""
|
|
}
|
|
return EncodedJwt(encoded)
|
|
}
|
|
|
|
// GenJwtForFilerServer creates a JSON-web-token for using the authenticated Filer API. Used f.e. inside
|
|
// the S3 API
|
|
func GenJwtForFilerServer(signingKey SigningKey, expiresAfterSec int) EncodedJwt {
|
|
if len(signingKey) == 0 {
|
|
return ""
|
|
}
|
|
|
|
claims := SeaweedFilerClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{},
|
|
}
|
|
if expiresAfterSec > 0 {
|
|
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(expiresAfterSec)))
|
|
}
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
encoded, e := t.SignedString([]byte(signingKey))
|
|
if e != nil {
|
|
glog.V(0).Infof("Failed to sign claims %+v: %v", t.Claims, e)
|
|
return ""
|
|
}
|
|
return EncodedJwt(encoded)
|
|
}
|
|
|
|
func GetJwt(r *http.Request) EncodedJwt {
|
|
|
|
// Get token from query params
|
|
tokenStr := r.URL.Query().Get("jwt")
|
|
|
|
// Get token from authorization header
|
|
if tokenStr == "" {
|
|
bearer := r.Header.Get("Authorization")
|
|
if len(bearer) > 7 && strings.ToUpper(bearer[0:6]) == "BEARER" {
|
|
tokenStr = bearer[7:]
|
|
}
|
|
}
|
|
|
|
// Get token from http only cookie
|
|
if tokenStr == "" {
|
|
token, err := r.Cookie("AT")
|
|
if err == nil {
|
|
tokenStr = token.Value
|
|
}
|
|
}
|
|
|
|
return EncodedJwt(tokenStr)
|
|
}
|
|
|
|
func DecodeJwt(signingKey SigningKey, tokenString EncodedJwt, claims jwt.Claims) (token *jwt.Token, err error) {
|
|
// check exp, nbf
|
|
return jwt.ParseWithClaims(string(tokenString), claims, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unknown token method")
|
|
}
|
|
return []byte(signingKey), nil
|
|
})
|
|
}
|
|
|
|
// FilerJwtProvider signs the credential a filer's HTTP API expects. A nil
|
|
// provider means the process-wide jwt.filer_signing configuration applies.
|
|
type FilerJwtProvider func(isWrite bool) EncodedJwt
|
|
|
|
// LoadFilerJwtFromFile reads jwt.filer_signing from a security file the way
|
|
// LoadClientTLSFromFile reads the TLS section, honoring the same WEED_
|
|
// environment precedence. A nil provider means the file configures no filer
|
|
// signing keys and the process-wide configuration applies. A file that sets
|
|
// only one access level falls back to the process-wide key for the other.
|
|
func LoadFilerJwtFromFile(configFile string) (FilerJwtProvider, error) {
|
|
v := viper.New()
|
|
v.SetConfigFile(configFile)
|
|
v.AutomaticEnv()
|
|
v.SetEnvPrefix("weed")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
if err := v.ReadInConfig(); err != nil {
|
|
return nil, fmt.Errorf("failed to read security config %s: %v", configFile, err)
|
|
}
|
|
|
|
signingKey := SigningKey(v.GetString("jwt.filer_signing.key"))
|
|
readSigningKey := SigningKey(v.GetString("jwt.filer_signing.read.key"))
|
|
if len(signingKey) == 0 && len(readSigningKey) == 0 {
|
|
return nil, nil
|
|
}
|
|
signingKeyExpires := v.GetInt("jwt.filer_signing.expires_after_seconds")
|
|
readSigningKeyExpires := v.GetInt("jwt.filer_signing.read.expires_after_seconds")
|
|
if len(signingKey) == 0 || len(readSigningKey) == 0 {
|
|
gv := util.GetViper()
|
|
if len(signingKey) == 0 {
|
|
signingKey = SigningKey(gv.GetString("jwt.filer_signing.key"))
|
|
signingKeyExpires = gv.GetInt("jwt.filer_signing.expires_after_seconds")
|
|
}
|
|
if len(readSigningKey) == 0 {
|
|
readSigningKey = SigningKey(gv.GetString("jwt.filer_signing.read.key"))
|
|
readSigningKeyExpires = gv.GetInt("jwt.filer_signing.read.expires_after_seconds")
|
|
}
|
|
}
|
|
if signingKeyExpires < 0 || readSigningKeyExpires < 0 {
|
|
return nil, fmt.Errorf("jwt.filer_signing lifetimes must not be negative")
|
|
}
|
|
if signingKeyExpires == 0 {
|
|
signingKeyExpires = 10
|
|
}
|
|
if readSigningKeyExpires == 0 {
|
|
readSigningKeyExpires = 60
|
|
}
|
|
return func(isWrite bool) EncodedJwt {
|
|
if isWrite {
|
|
return GenJwtForFilerServer(signingKey, signingKeyExpires)
|
|
}
|
|
return GenJwtForFilerServer(readSigningKey, readSigningKeyExpires)
|
|
}, nil
|
|
}
|