mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 04:50:54 +02:00
s3: tighten STS session token handling (#11383)
* s3api: test that a session token must not reveal its credential * sts: derive secret access key with HMAC keyed on the signing key * s3api: stop accepting STS session tokens as bearer credentials * security: reject STS session tokens on filer and admin gRPC auth * test: sign s3/iam framework requests with the session credential * s3api: exercise the real auth pipeline in the end-to-end harness
This commit is contained in:
@@ -2,9 +2,12 @@ package iam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -354,12 +357,12 @@ func (t *BearerTokenTransport) extractPrincipalFromJWT(tokenString string) strin
|
||||
}
|
||||
|
||||
// generateSTSSessionToken creates a session token using the actual STS service for proper validation
|
||||
func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, validDuration time.Duration, account string, customClaims map[string]interface{}) (string, error) {
|
||||
func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, validDuration time.Duration, account string, customClaims map[string]interface{}) (string, *credentials.Credentials, error) {
|
||||
now := time.Now()
|
||||
signingKeyB64 := "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc="
|
||||
signingKey, err := base64.StdEncoding.DecodeString(signingKeyB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode signing key: %v", err)
|
||||
return "", nil, fmt.Errorf("failed to decode signing key: %v", err)
|
||||
}
|
||||
|
||||
// Generate a session ID that would be created by the STS service
|
||||
@@ -404,10 +407,19 @@ func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string,
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims)
|
||||
tokenString, err := token.SignedString(signingKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
accessKeyHash := sha256.Sum256([]byte("access-key:" + sessionId))
|
||||
mac := hmac.New(sha256.New, signingKey)
|
||||
mac.Write([]byte("secret-key:" + sessionId))
|
||||
creds := credentials.NewStaticCredentials(
|
||||
"ASIA"+hex.EncodeToString(accessKeyHash[:8]),
|
||||
base64.StdEncoding.EncodeToString(mac.Sum(nil)),
|
||||
tokenString,
|
||||
)
|
||||
|
||||
return tokenString, creds, nil
|
||||
}
|
||||
|
||||
// CreateS3ClientWithJWT creates an S3 client authenticated with a JWT token for the specified role
|
||||
@@ -417,36 +429,34 @@ func (f *S3IAMTestFramework) CreateS3ClientWithJWT(username, roleName string) (*
|
||||
|
||||
// CreateS3ClientWithCustomClaims creates an S3 client with specific account ID and custom claims
|
||||
func (f *S3IAMTestFramework) CreateS3ClientWithCustomClaims(username, roleName, account string, claims map[string]interface{}) (*s3.S3, error) {
|
||||
var token string
|
||||
var err error
|
||||
var httpClient *http.Client
|
||||
creds := credentials.AnonymousCredentials
|
||||
|
||||
if f.useKeycloak && claims == nil && account == "" {
|
||||
// Use real Keycloak authentication if no custom requirements
|
||||
token, err = f.getKeycloakToken(username)
|
||||
token, err := f.getKeycloakToken(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Keycloak token: %v", err)
|
||||
}
|
||||
httpClient = &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Generate STS session token (mock mode or custom requirements)
|
||||
token, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
var err error
|
||||
_, creds, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate STS session token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom HTTP client with Bearer token transport
|
||||
httpClient := &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
// Use anonymous credentials to avoid AWS signature generation
|
||||
Credentials: credentials.AnonymousCredentials,
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
Credentials: creds,
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
})
|
||||
@@ -487,7 +497,7 @@ func (f *S3IAMTestFramework) CreateS3ClientWithInvalidJWT() (*s3.S3, error) {
|
||||
// CreateS3ClientWithExpiredJWT creates an S3 client with an expired JWT token
|
||||
func (f *S3IAMTestFramework) CreateS3ClientWithExpiredJWT(username, roleName string) (*s3.S3, error) {
|
||||
// Generate expired STS session token (expired 1 hour ago)
|
||||
token, err := f.generateSTSSessionToken(username, roleName, -time.Hour, "", nil)
|
||||
token, _, err := f.generateSTSSessionToken(username, roleName, -time.Hour, "", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate expired STS session token: %v", err)
|
||||
}
|
||||
@@ -942,36 +952,34 @@ func (f *S3IAMTestFramework) CreateIAMClientWithJWT(username, roleName string) (
|
||||
|
||||
// CreateIAMClientWithCustomClaims creates an IAM client with specific account ID and custom claims
|
||||
func (f *S3IAMTestFramework) CreateIAMClientWithCustomClaims(username, roleName, account string, claims map[string]interface{}) (*iam.IAM, error) {
|
||||
var token string
|
||||
var err error
|
||||
var httpClient *http.Client
|
||||
creds := credentials.AnonymousCredentials
|
||||
|
||||
if f.useKeycloak && claims == nil && account == "" {
|
||||
// Use real Keycloak authentication if no custom requirements
|
||||
token, err = f.getKeycloakToken(username)
|
||||
token, err := f.getKeycloakToken(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Keycloak token: %v", err)
|
||||
}
|
||||
httpClient = &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Generate STS session token (mock mode or custom requirements)
|
||||
token, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
var err error
|
||||
_, creds, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate STS session token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom HTTP client with Bearer token transport
|
||||
httpClient := &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
// Use anonymous credentials to avoid AWS signature generation
|
||||
Credentials: credentials.AnonymousCredentials,
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
Credentials: creds,
|
||||
DisableSSL: aws.Bool(true),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -693,23 +694,25 @@ type ListGroupsResponse struct {
|
||||
func callIAMAPIAuthenticated(_ *testing.T, framework *S3IAMTestFramework, action string, params url.Values) (*http.Response, error) {
|
||||
params.Set("Action", action)
|
||||
|
||||
body := params.Encode()
|
||||
req, err := http.NewRequest(http.MethodPost, TestIAMEndpoint+"/",
|
||||
strings.NewReader(params.Encode()))
|
||||
strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
token, err := framework.generateSTSSessionToken("admin-user", "TestAdminRole", time.Hour, "", nil)
|
||||
_, creds, err := framework.generateSTSSessionToken("admin-user", "TestAdminRole", time.Hour, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &BearerTokenTransport{Token: token},
|
||||
if _, err := v4.NewSigner(creds).Sign(req, strings.NewReader(body), "iam", TestRegion, time.Now()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestTemporaryCredentialPrefix(t *testing.T) {
|
||||
sessionId := "test-session-for-prefix"
|
||||
expiration := time.Now().Add(time.Hour)
|
||||
|
||||
credGen := NewCredentialGenerator()
|
||||
credGen := testCredGen
|
||||
cred, err := credGen.GenerateTemporaryCredentials(sessionId, expiration)
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -37,7 +37,7 @@ func TestTemporaryCredentialFormat(t *testing.T) {
|
||||
sessionId := "format-test-session"
|
||||
expiration := time.Now().Add(time.Hour)
|
||||
|
||||
credGen := NewCredentialGenerator()
|
||||
credGen := testCredGen
|
||||
cred, err := credGen.GenerateTemporaryCredentials(sessionId, expiration)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestSessionClaimsRoundTripParentUser(t *testing.T) {
|
||||
WithRoleInfo("arn:aws:iam::123:role/r", "arn:aws:sts::123:assumed-role/r/s", "arn:aws:sts::123:assumed-role/r/s").
|
||||
WithParentUser(parent)
|
||||
|
||||
info := claims.ToSessionInfo()
|
||||
info := claims.ToSessionInfo(testCredGen)
|
||||
if info.ParentUser != parent {
|
||||
t.Fatalf("ParentUser lost on round-trip: got %q want %q", info.ParentUser, parent)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package sts
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,11 +53,6 @@ func ResolveIdentityClaim(ctx map[string]interface{}) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// defaultCredentialGenerator is a reusable instance for generating temporary credentials
|
||||
// Reusing a single instance across all calls to ToSessionInfo() reduces allocation overhead
|
||||
// since this method may be called frequently during signature verification
|
||||
var defaultCredentialGenerator = NewCredentialGenerator()
|
||||
|
||||
// STSSessionClaims represents comprehensive session information embedded in JWT tokens
|
||||
// This eliminates the need for separate session storage by embedding all session
|
||||
// metadata directly in the token itself - enabling true stateless operation
|
||||
@@ -122,21 +116,20 @@ func NewSTSSessionClaims(sessionId, issuer string, expiresAt time.Time) *STSSess
|
||||
|
||||
// ToSessionInfo converts JWT claims back to SessionInfo structure
|
||||
// This enables seamless integration with existing code expecting SessionInfo
|
||||
func (c *STSSessionClaims) ToSessionInfo() *SessionInfo {
|
||||
func (c *STSSessionClaims) ToSessionInfo(credGen *CredentialGenerator) *SessionInfo {
|
||||
var expiresAt time.Time
|
||||
if c.ExpiresAt != nil {
|
||||
expiresAt = c.ExpiresAt.Time
|
||||
}
|
||||
|
||||
// Generate temporary credentials from the session ID
|
||||
// This is deterministic based on the session ID, so the same credentials are regenerated
|
||||
credentials, err := defaultCredentialGenerator.GenerateTemporaryCredentials(c.SessionId, expiresAt)
|
||||
if err != nil {
|
||||
// Log the error with context - credential generation failure is important for debugging
|
||||
errMsg := fmt.Errorf("generate temporary credentials for session %s: %w", c.SessionId, err)
|
||||
glog.Warningf("Failed to generate credentials for STS session: %v", errMsg)
|
||||
// Return session info without credentials - validation will catch this as invalid
|
||||
credentials = nil
|
||||
var credentials *Credentials
|
||||
if credGen != nil {
|
||||
creds, err := credGen.GenerateTemporaryCredentials(c.SessionId, expiresAt)
|
||||
if err != nil {
|
||||
glog.Warningf("Failed to generate credentials for STS session %s: %v", c.SessionId, err)
|
||||
} else {
|
||||
credentials = creds
|
||||
}
|
||||
}
|
||||
|
||||
return &SessionInfo{
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testCredGen = NewCredentialGenerator([]byte("test-signing-key"))
|
||||
|
||||
// TestSTSSessionClaimsToSessionInfo tests the ToSessionInfo conversion
|
||||
func TestSTSSessionClaimsToSessionInfo(t *testing.T) {
|
||||
sessionId := "test-session-123"
|
||||
@@ -24,7 +26,7 @@ func TestSTSSessionClaimsToSessionInfo(t *testing.T) {
|
||||
WithIdentityProvider("oidc", "user-123", "https://issuer.example.com").
|
||||
WithMaxDuration(time.Hour)
|
||||
|
||||
sessionInfo := claims.ToSessionInfo()
|
||||
sessionInfo := claims.ToSessionInfo(testCredGen)
|
||||
|
||||
// Verify basic claims are converted
|
||||
assert.Equal(t, sessionId, sessionInfo.SessionId)
|
||||
@@ -52,11 +54,11 @@ func TestSTSSessionClaimsToSessionInfoCredentialGeneration(t *testing.T) {
|
||||
expiresAt := time.Now().Add(time.Hour).Truncate(time.Second)
|
||||
|
||||
claims1 := NewSTSSessionClaims(sessionId, issuer, expiresAt)
|
||||
sessionInfo1 := claims1.ToSessionInfo()
|
||||
sessionInfo1 := claims1.ToSessionInfo(testCredGen)
|
||||
|
||||
// Create another claims object with the same session ID and expiration
|
||||
claims2 := NewSTSSessionClaims(sessionId, issuer, expiresAt)
|
||||
sessionInfo2 := claims2.ToSessionInfo()
|
||||
sessionInfo2 := claims2.ToSessionInfo(testCredGen)
|
||||
|
||||
// Verify that both have valid credentials
|
||||
assert.NotNil(t, sessionInfo1.Credentials, "credentials should be populated")
|
||||
@@ -104,7 +106,7 @@ func TestSTSSessionClaimsToSessionInfoPreservesAllFields(t *testing.T) {
|
||||
WithRequestContext(requestContext).
|
||||
WithMaxDuration(2 * time.Hour)
|
||||
|
||||
sessionInfo := claims.ToSessionInfo()
|
||||
sessionInfo := claims.ToSessionInfo(testCredGen)
|
||||
|
||||
// Verify all fields are preserved
|
||||
assert.Equal(t, sessionId, sessionInfo.SessionId)
|
||||
@@ -130,7 +132,7 @@ func TestSTSSessionClaimsToSessionInfoEmptyFields(t *testing.T) {
|
||||
// Create claims with minimal fields
|
||||
claims := NewSTSSessionClaims(sessionId, issuer, expiresAt)
|
||||
|
||||
sessionInfo := claims.ToSessionInfo()
|
||||
sessionInfo := claims.ToSessionInfo(testCredGen)
|
||||
|
||||
// Verify basic fields are preserved
|
||||
assert.Equal(t, sessionId, sessionInfo.SessionId)
|
||||
@@ -177,7 +179,7 @@ func TestSTSSessionClaimsToSessionInfoCredentialExpiration(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
claims := NewSTSSessionClaims(sessionId, issuer, tc.expiresAt)
|
||||
sessionInfo := claims.ToSessionInfo()
|
||||
sessionInfo := claims.ToSessionInfo(testCredGen)
|
||||
|
||||
assert.NotNil(t, sessionInfo.Credentials)
|
||||
// Check expiration within 1 second due to timing precision (symmetric tolerance)
|
||||
@@ -211,7 +213,7 @@ func TestSessionInfoIntegration(t *testing.T) {
|
||||
WithIdentityProvider("test-provider", "user-id", "https://test.example.com")
|
||||
|
||||
// Convert to SessionInfo
|
||||
sessionInfo := claims.ToSessionInfo()
|
||||
sessionInfo := claims.ToSessionInfo(testCredGen)
|
||||
|
||||
// Verify the session info has valid credentials
|
||||
assert.NotNil(t, sessionInfo.Credentials)
|
||||
@@ -238,7 +240,7 @@ func TestSecretAccessKeyDeterminism(t *testing.T) {
|
||||
expiration := time.Now().Add(time.Hour)
|
||||
|
||||
// Generate credentials multiple times with the same session ID
|
||||
credGen := NewCredentialGenerator()
|
||||
credGen := testCredGen
|
||||
|
||||
cred1, err := credGen.GenerateTemporaryCredentials(sessionId, expiration)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -94,6 +94,7 @@ type STSService struct {
|
||||
providers map[string]providers.IdentityProvider
|
||||
issuerToProvider map[string]providers.IdentityProvider // Efficient issuer-based provider lookup
|
||||
tokenGenerator *TokenGenerator
|
||||
credGenerator *CredentialGenerator
|
||||
trustPolicyValidator TrustPolicyValidator // Interface for trust policy validation
|
||||
|
||||
// iamManagedOIDCMu guards iamManagedOIDCByIssuer. The map is the live view
|
||||
@@ -128,6 +129,11 @@ func (s *STSService) GetTokenGenerator() *TokenGenerator {
|
||||
return s.tokenGenerator
|
||||
}
|
||||
|
||||
// GetCredentialGenerator returns the credential generator used by the STS service.
|
||||
func (s *STSService) GetCredentialGenerator() *CredentialGenerator {
|
||||
return s.credGenerator
|
||||
}
|
||||
|
||||
// STSConfig holds STS service configuration
|
||||
type STSConfig struct {
|
||||
// TokenDuration is the default duration for issued tokens
|
||||
@@ -325,6 +331,7 @@ func (s *STSService) Initialize(config *STSConfig) error {
|
||||
|
||||
// Initialize token generator for stateless JWT operations
|
||||
s.tokenGenerator = NewTokenGenerator(config.SigningKey, config.Issuer)
|
||||
s.credGenerator = NewCredentialGenerator(config.SigningKey)
|
||||
|
||||
// Load identity providers from configuration
|
||||
if err := s.loadProvidersFromConfig(config); err != nil {
|
||||
@@ -606,8 +613,7 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *Ass
|
||||
return nil, fmt.Errorf("failed to generate session ID: %w", err)
|
||||
}
|
||||
|
||||
credGenerator := NewCredentialGenerator()
|
||||
credentials, err := credGenerator.GenerateTemporaryCredentials(sessionId, expiresAt)
|
||||
credentials, err := s.credGenerator.GenerateTemporaryCredentials(sessionId, expiresAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate credentials: %w", err)
|
||||
}
|
||||
@@ -771,8 +777,7 @@ func (s *STSService) issueSession(roleArn, roleSessionName, sessionPolicy string
|
||||
return nil, fmt.Errorf("failed to generate session ID: %w", err)
|
||||
}
|
||||
|
||||
credGenerator := NewCredentialGenerator()
|
||||
tempCredentials, err := credGenerator.GenerateTemporaryCredentials(sessionId, expiresAt)
|
||||
tempCredentials, err := s.credGenerator.GenerateTemporaryCredentials(sessionId, expiresAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate credentials: %w", err)
|
||||
}
|
||||
@@ -909,7 +914,7 @@ func (s *STSService) ValidateSessionToken(ctx context.Context, sessionToken stri
|
||||
|
||||
// Convert JWT claims back to SessionInfo
|
||||
// All session information is embedded in the JWT token itself
|
||||
return claims.ToSessionInfo(), nil
|
||||
return claims.ToSessionInfo(s.credGenerator), nil
|
||||
}
|
||||
|
||||
// NOTE: Session revocation is not supported in the stateless JWT design.
|
||||
|
||||
+15
-13
@@ -1,6 +1,7 @@
|
||||
package sts
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
@@ -163,11 +164,15 @@ type SessionTokenClaims struct {
|
||||
}
|
||||
|
||||
// CredentialGenerator generates AWS-compatible temporary credentials
|
||||
type CredentialGenerator struct{}
|
||||
type CredentialGenerator struct {
|
||||
signingKey []byte
|
||||
}
|
||||
|
||||
// NewCredentialGenerator creates a new credential generator
|
||||
func NewCredentialGenerator() *CredentialGenerator {
|
||||
return &CredentialGenerator{}
|
||||
// NewCredentialGenerator creates a new credential generator. The signing key
|
||||
// keys the secret access key derivation so it cannot be recomputed from the
|
||||
// session id embedded in the session token.
|
||||
func NewCredentialGenerator(signingKey []byte) *CredentialGenerator {
|
||||
return &CredentialGenerator{signingKey: signingKey}
|
||||
}
|
||||
|
||||
// GenerateTemporaryCredentials creates temporary AWS credentials
|
||||
@@ -203,16 +208,13 @@ func (c *CredentialGenerator) generateTemporaryAccessKeyId(sessionId string) (st
|
||||
return "ASIA" + hex.EncodeToString(hash[:8]), nil // AWS format: ASIA + 16 chars
|
||||
}
|
||||
|
||||
// generateSecretAccessKey generates a deterministic secret access key based on sessionId
|
||||
// This ensures the same secret key is regenerated from the JWT claims during signature verification
|
||||
// generateSecretAccessKey derives the secret access key from the session id
|
||||
// keyed on the STS signing key. Issuance and the regeneration in ToSessionInfo
|
||||
// agree on the value, but it cannot be recomputed from the token alone.
|
||||
func (c *CredentialGenerator) generateSecretAccessKey(sessionId string) (string, error) {
|
||||
// Create deterministic secret key based on session ID (not random!)
|
||||
// This is critical for STS because:
|
||||
// 1. AssumeRoleWithWebIdentity generates the secret key once
|
||||
// 2. During signature verification, ToSessionInfo() regenerates credentials from JWT
|
||||
// 3. Both must generate the same secret key for signature verification to succeed
|
||||
hash := sha256.Sum256([]byte("secret-key:" + sessionId))
|
||||
return base64.StdEncoding.EncodeToString(hash[:]), nil
|
||||
mac := hmac.New(sha256.New, c.signingKey)
|
||||
mac.Write([]byte("secret-key:" + sessionId))
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// generateSessionTokenId generates a session token identifier
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
)
|
||||
|
||||
// sessionIdFromToken reads the session id out of a session token (or a
|
||||
// presigned URL's X-Amz-Security-Token) without any key.
|
||||
func sessionIdFromToken(t *testing.T, token string) string {
|
||||
t.Helper()
|
||||
parts := strings.Split(token, ".")
|
||||
require.Len(t, parts, 3)
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
require.NoError(t, err)
|
||||
var claims map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(payload, &claims))
|
||||
sid, ok := claims["sid"].(string)
|
||||
require.True(t, ok, "session token should carry a sid claim")
|
||||
return sid
|
||||
}
|
||||
|
||||
// issueTestSession mints an STS session the way AssumeRoleWithWebIdentity does.
|
||||
func issueTestSession(t *testing.T, stsService *sts.STSService, config *sts.STSConfig) (*sts.SessionInfo, string) {
|
||||
t.Helper()
|
||||
sessionId, err := sts.GenerateSessionId()
|
||||
require.NoError(t, err)
|
||||
claims := sts.NewSTSSessionClaims(sessionId, config.Issuer, time.Now().Add(time.Hour)).
|
||||
WithSessionName("alice-session").
|
||||
WithRoleInfo("arn:aws:iam::role/AppRole",
|
||||
"arn:aws:sts::assumed-role/AppRole/alice-session",
|
||||
"arn:aws:sts::assumed-role/AppRole/alice-session")
|
||||
token, err := sts.NewTokenGenerator(config.SigningKey, config.Issuer).GenerateJWTWithClaims(claims)
|
||||
require.NoError(t, err)
|
||||
sessionInfo, err := stsService.ValidateSessionToken(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
return sessionInfo, token
|
||||
}
|
||||
|
||||
// testIdentityFromSessionToken builds the identity for a validated STS session
|
||||
// token. Session tokens are not bearer credentials; tests use this to reach
|
||||
// the authorization layer the way a verified SigV4 request would.
|
||||
func testIdentityFromSessionToken(t *testing.T, s3iam *S3IAMIntegration, sessionToken string) *IAMIdentity {
|
||||
t.Helper()
|
||||
sessionInfo, err := s3iam.stsService.ValidateSessionToken(context.Background(), sessionToken)
|
||||
require.NoError(t, err)
|
||||
claims := make(map[string]interface{}, len(sessionInfo.RequestContext)+4)
|
||||
for k, v := range sessionInfo.RequestContext {
|
||||
claims[k] = v
|
||||
}
|
||||
claims["sub"] = sessionInfo.Subject
|
||||
claims["role"] = sessionInfo.RoleArn
|
||||
claims["principal"] = sessionInfo.Principal
|
||||
claims["snam"] = sessionInfo.SessionName
|
||||
return &IAMIdentity{
|
||||
Name: sessionInfo.Subject,
|
||||
Principal: sessionInfo.Principal,
|
||||
SessionToken: sessionToken,
|
||||
Account: &Account{
|
||||
DisplayName: sessionInfo.SessionName,
|
||||
EmailAddress: sessionInfo.Subject + "@seaweedfs.local",
|
||||
Id: sessionInfo.Subject,
|
||||
},
|
||||
Claims: claims,
|
||||
}
|
||||
}
|
||||
|
||||
// A presigned URL discloses the session token in X-Amz-Security-Token. Whoever
|
||||
// holds it must not be able to reconstruct the temporary credential or turn it
|
||||
// into a standalone credential.
|
||||
func TestSTSSessionTokenDoesNotRevealCredential(t *testing.T) {
|
||||
stsService, config := setupTestSTSService(t)
|
||||
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, nil, "memory")
|
||||
s3iam := &S3IAMIntegration{stsService: stsService, enabled: true}
|
||||
iam.SetIAMIntegration(s3iam)
|
||||
|
||||
sessionInfo, sessionToken := issueTestSession(t, stsService, config)
|
||||
|
||||
// control: the issued credential verifies
|
||||
req, err := newTestRequest(http.MethodGet, "https://example.com/reports/data.csv", 0, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("X-Amz-Security-Token", sessionToken)
|
||||
require.NoError(t, signRequestV4(req, sessionInfo.Credentials.AccessKeyId, sessionInfo.Credentials.SecretAccessKey))
|
||||
_, errCode := iam.reqSignatureV4Verify(req)
|
||||
require.Equal(t, s3err.ErrNone, errCode)
|
||||
|
||||
// attack: recompute the credential from the token's public claims
|
||||
sid := sessionIdFromToken(t, sessionToken)
|
||||
akHash := sha256.Sum256([]byte("access-key:" + sid))
|
||||
accessKey := "ASIA" + hex.EncodeToString(akHash[:8])
|
||||
skHash := sha256.Sum256([]byte("secret-key:" + sid))
|
||||
secretKey := base64.StdEncoding.EncodeToString(skHash[:])
|
||||
|
||||
forged, err := newTestRequest(http.MethodDelete, "https://example.com/reports/payroll.csv", 0, nil)
|
||||
require.NoError(t, err)
|
||||
forged.Header.Set("X-Amz-Security-Token", sessionToken)
|
||||
require.NoError(t, signRequestV4(forged, accessKey, secretKey))
|
||||
_, errCode = iam.reqSignatureV4Verify(forged)
|
||||
require.NotEqual(t, s3err.ErrNone, errCode,
|
||||
"a request signed with a credential derived from the session token must not verify")
|
||||
|
||||
// attack: replay the token itself as a bearer credential
|
||||
bearerReq := httptest.NewRequest(http.MethodDelete, "/reports/payroll.csv", http.NoBody)
|
||||
bearerReq.Header.Set("Authorization", "Bearer "+sessionToken)
|
||||
_, errCode = s3iam.AuthenticateJWT(context.Background(), bearerReq)
|
||||
require.NotEqual(t, s3err.ErrNone, errCode,
|
||||
"an STS session token must not authenticate as a bearer token")
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func TestAuthorizeWithIAMSessionTokenExtraction(t *testing.T) {
|
||||
// preserved when converting to credentials for authorization.
|
||||
func TestSTSSessionTokenIntoCredentials(t *testing.T) {
|
||||
// Create a credential generator and session claims
|
||||
credGen := sts.NewCredentialGenerator()
|
||||
credGen := sts.NewCredentialGenerator([]byte("test-signing-key"))
|
||||
sessionId := "test-session-123"
|
||||
expiresAt := time.Now().Add(time.Hour)
|
||||
|
||||
|
||||
@@ -114,13 +114,12 @@ func TestS3EndToEndWithJWT(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err, "Failed to assume role %s", tt.roleArn)
|
||||
|
||||
jwtToken := response.Credentials.SessionToken
|
||||
require.NotEmpty(t, jwtToken, "JWT token should not be empty")
|
||||
require.NotEmpty(t, response.Credentials.SessionToken, "session token should not be empty")
|
||||
|
||||
// Execute S3 operations
|
||||
for i, operation := range tt.s3Operations {
|
||||
t.Run(fmt.Sprintf("%s_%s", tt.name, operation.Operation), func(t *testing.T) {
|
||||
allowed := executeS3OperationWithJWT(t, s3Server, operation, jwtToken)
|
||||
allowed := executeS3OperationWithJWT(t, s3Server, operation, response.Credentials)
|
||||
expected := tt.expectedResults[i]
|
||||
|
||||
if expected {
|
||||
@@ -153,8 +152,6 @@ func TestS3MultipartUploadWithJWT(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtToken := response.Credentials.SessionToken
|
||||
|
||||
// Test multipart upload workflow
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -205,7 +202,7 @@ func TestS3MultipartUploadWithJWT(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
allowed := executeS3OperationWithJWT(t, s3Server, tt.operation, jwtToken)
|
||||
allowed := executeS3OperationWithJWT(t, s3Server, tt.operation, response.Credentials)
|
||||
if tt.expected {
|
||||
assert.True(t, allowed, "Multipart operation %s should be allowed", tt.operation.Operation)
|
||||
} else {
|
||||
@@ -304,10 +301,7 @@ func TestS3ListObjectsV2PrefixCondition(t *testing.T) {
|
||||
require.NotEmpty(t, sessionToken)
|
||||
|
||||
// Authenticate to get IAM identity
|
||||
authReq := httptest.NewRequest("GET", "/examples", http.NoBody)
|
||||
authReq.Header.Set("Authorization", "Bearer "+sessionToken)
|
||||
identity, errCode := s3IAMIntegration.AuthenticateJWT(ctx, authReq)
|
||||
require.Equal(t, s3err.ErrNone, errCode, "Authentication should succeed")
|
||||
identity := testIdentityFromSessionToken(t, s3IAMIntegration, sessionToken)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -434,10 +428,7 @@ func TestS3CreateBucketWithAttachedPolicy(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
authReq := httptest.NewRequest("PUT", "/ml-models", http.NoBody)
|
||||
authReq.Header.Set("Authorization", "Bearer "+response.Credentials.SessionToken)
|
||||
identity, errCode := s3IAMIntegration.AuthenticateJWT(ctx, authReq)
|
||||
require.Equal(t, s3err.ErrNone, errCode)
|
||||
identity := testIdentityFromSessionToken(t, s3IAMIntegration, response.Credentials.SessionToken)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -538,8 +529,6 @@ func TestS3PerformanceWithIAM(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtToken := response.Credentials.SessionToken
|
||||
|
||||
// Benchmark multiple GET requests
|
||||
numRequests := 100
|
||||
start := time.Now()
|
||||
@@ -552,7 +541,7 @@ func TestS3PerformanceWithIAM(t *testing.T) {
|
||||
Operation: "GetObject",
|
||||
}
|
||||
|
||||
executeS3OperationWithJWT(t, s3Server, operation, jwtToken)
|
||||
executeS3OperationWithJWT(t, s3Server, operation, response.Credentials)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@@ -629,11 +618,13 @@ func setupCompleteS3IAMSystem(t *testing.T) (http.Handler, *integration.IAMManag
|
||||
t.Skip("Could not create S3 IAM integration")
|
||||
}
|
||||
|
||||
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, nil, "memory")
|
||||
iam.SetIAMIntegration(s3IAMIntegration)
|
||||
|
||||
// Add a simple test endpoint that we can use to verify IAM functionality
|
||||
router.HandleFunc("/test-auth", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Test JWT authentication
|
||||
identity, errCode := s3IAMIntegration.AuthenticateJWT(r.Context(), r)
|
||||
if errCode != s3err.ErrNone {
|
||||
identity, errCode, _ := iam.authenticateRequestInternal(r)
|
||||
if errCode != s3err.ErrNone || identity == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Authentication failed"))
|
||||
return
|
||||
@@ -655,7 +646,7 @@ func setupCompleteS3IAMSystem(t *testing.T) (http.Handler, *integration.IAMManag
|
||||
}
|
||||
|
||||
// Test authorization with appropriate action
|
||||
authErrCode := s3IAMIntegration.AuthorizeAction(r.Context(), identity, action, "test-bucket", "test-object", r)
|
||||
authErrCode := iam.authorizeWithIAM(r, identity, action, "test-bucket", "test-object")
|
||||
if authErrCode != s3err.ErrNone {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Authorization failed"))
|
||||
@@ -900,10 +891,11 @@ func setupS3IPRestrictedRole(ctx context.Context, manager *integration.IAMManage
|
||||
})
|
||||
}
|
||||
|
||||
func executeS3OperationWithJWT(t *testing.T, s3Server http.Handler, operation S3Operation, jwtToken string) bool {
|
||||
func executeS3OperationWithJWT(t *testing.T, s3Server http.Handler, operation S3Operation, creds *sts.Credentials) bool {
|
||||
// Use our simplified test endpoint for IAM validation with the correct HTTP method
|
||||
req := httptest.NewRequest(operation.Method, "/test-auth", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+jwtToken)
|
||||
req, err := newTestRequest(operation.Method, "http://example.com/test-auth", 0, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("X-Amz-Security-Token", creds.SessionToken)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
// Set source IP if specified
|
||||
@@ -912,6 +904,8 @@ func executeS3OperationWithJWT(t *testing.T, s3Server http.Handler, operation S3
|
||||
req.RemoteAddr = operation.SourceIP + ":12345"
|
||||
}
|
||||
|
||||
require.NoError(t, signRequestV4(req, creds.AccessKeyId, creds.SecretAccessKey))
|
||||
|
||||
// Execute request
|
||||
recorder := httptest.NewRecorder()
|
||||
s3Server.ServeHTTP(recorder, req)
|
||||
|
||||
@@ -220,9 +220,7 @@ func groupsRolesJWT(t *testing.T, subject string, groups, roles []string) string
|
||||
}
|
||||
|
||||
func authenticateSessionToken(t *testing.T, s3iam *S3IAMIntegration, sessionToken string) (*IAMIdentity, s3err.ErrorCode) {
|
||||
req := httptest.NewRequest("GET", "/", http.NoBody)
|
||||
req.Header.Set("Authorization", "Bearer "+sessionToken)
|
||||
return s3iam.AuthenticateJWT(context.Background(), req)
|
||||
return testIdentityFromSessionToken(t, s3iam, sessionToken), s3err.ErrNone
|
||||
}
|
||||
|
||||
func authorizeGet(ctx context.Context, s3iam *S3IAMIntegration, identity *IAMIdentity, bucket string) bool {
|
||||
|
||||
@@ -189,51 +189,12 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
||||
}, s3err.ErrNone
|
||||
}
|
||||
|
||||
// This is an STS-issued token - validate with STS service
|
||||
// ValidateSessionToken performs cryptographic verification and extraction of trusted claims
|
||||
sessionInfo, err := s3iam.stsService.ValidateSessionToken(ctx, sessionToken)
|
||||
if err != nil {
|
||||
glog.V(3).Infof("STS session validation failed: %v", err)
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Create claims map starting with request context (which holds custom claims)
|
||||
claims := make(map[string]interface{})
|
||||
if sessionInfo.RequestContext != nil {
|
||||
for k, v := range sessionInfo.RequestContext {
|
||||
claims[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Add standard claims
|
||||
claims["sub"] = sessionInfo.Subject
|
||||
claims["role"] = sessionInfo.RoleArn
|
||||
claims["principal"] = sessionInfo.Principal
|
||||
claims["snam"] = sessionInfo.SessionName
|
||||
|
||||
// Create IAM identity from VALIDATED session info
|
||||
// We use the trusted data returned by the STS service, not the unverified token claims
|
||||
identity := &IAMIdentity{
|
||||
Name: sessionInfo.Subject,
|
||||
Principal: sessionInfo.Principal,
|
||||
SessionToken: sessionToken,
|
||||
Account: &Account{
|
||||
DisplayName: sessionInfo.SessionName,
|
||||
EmailAddress: sessionInfo.Subject + "@seaweedfs.local",
|
||||
Id: sessionInfo.Subject,
|
||||
},
|
||||
Claims: claims,
|
||||
}
|
||||
// ParentUser is set only for OIDC-federated sessions. Resolve the audit
|
||||
// identity claim from the original request context (not the local claims
|
||||
// map, whose sub was overwritten with the opaque session subject above) so
|
||||
// the bearer path surfaces the same authoritative OIDC identity as SigV4.
|
||||
if sessionInfo.ParentUser != "" {
|
||||
identity.IdentityClaim = sts.ResolveIdentityClaim(sessionInfo.RequestContext)
|
||||
}
|
||||
|
||||
glog.V(3).Infof("JWT authentication successful for principal: %s", identity.Principal)
|
||||
return identity, s3err.ErrNone
|
||||
// STS session tokens authenticate SigV4 requests via proof of possession
|
||||
// (signature with the derived secret). As bearer tokens they would turn
|
||||
// every presigned URL, which carries the token in X-Amz-Security-Token,
|
||||
// into a standalone credential.
|
||||
glog.V(3).Infof("Rejected STS session token presented as bearer token")
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// ValidateSessionToken checks the validity of an STS session token
|
||||
|
||||
@@ -100,9 +100,9 @@ func TestJWTAuthenticationFlow(t *testing.T) {
|
||||
// Test each operation
|
||||
for _, op := range tt.testOperations {
|
||||
t.Run(string(op.Action), func(t *testing.T) {
|
||||
// Test JWT authentication
|
||||
identity, errCode := testJWTAuthentication(t, iamServer, jwtToken)
|
||||
require.Equal(t, s3err.ErrNone, errCode, "JWT authentication should succeed")
|
||||
// Test session authentication via SigV4 proof of possession
|
||||
identity, errCode := testSessionAuthentication(t, iamServer, response.Credentials)
|
||||
require.Equal(t, s3err.ErrNone, errCode, "Session authentication should succeed")
|
||||
require.NotNil(t, identity)
|
||||
|
||||
// Test authorization with appropriate role based on test case
|
||||
@@ -537,6 +537,16 @@ func setupTestIPRestrictedRole(ctx context.Context, manager *integration.IAMMana
|
||||
})
|
||||
}
|
||||
|
||||
// testSessionAuthentication authenticates STS temporary credentials the way a
|
||||
// real client does: a SigV4-signed request carrying the session token.
|
||||
func testSessionAuthentication(t *testing.T, iam *IdentityAccessManagement, creds *sts.Credentials) (*Identity, s3err.ErrorCode) {
|
||||
req, err := newTestRequest("GET", "https://example.com/test-bucket/test-object", 0, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("X-Amz-Security-Token", creds.SessionToken)
|
||||
require.NoError(t, signRequestV4(req, creds.AccessKeyId, creds.SecretAccessKey))
|
||||
return iam.reqSignatureV4Verify(req)
|
||||
}
|
||||
|
||||
func testJWTAuthentication(t *testing.T, iam *IdentityAccessManagement, token string) (*Identity, s3err.ErrorCode) {
|
||||
// Create test request with JWT
|
||||
req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
|
||||
|
||||
@@ -46,6 +46,9 @@ func (s3a *S3ApiServer) checkAdminAuth(ctx context.Context) error {
|
||||
if err != nil || parsed == nil || !parsed.Valid {
|
||||
return status.Error(codes.Unauthenticated, "invalid admin token")
|
||||
}
|
||||
if claims, ok := parsed.Claims.(*security.SeaweedFilerAdminClaims); !ok || claims.SessionId != "" {
|
||||
return status.Error(codes.Unauthenticated, "invalid admin token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -802,7 +802,7 @@ func (h *STSHandlers) handleGetFederationToken(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
// Generate temporary credentials
|
||||
stsCredGen := sts.NewCredentialGenerator()
|
||||
stsCredGen := h.stsService.GetCredentialGenerator()
|
||||
stsCredsDet, err := stsCredGen.GenerateTemporaryCredentials(sessionId, expiration)
|
||||
if err != nil {
|
||||
h.writeSTSErrorResponse(w, r, STSErrInternalError,
|
||||
@@ -921,7 +921,7 @@ func (h *STSHandlers) prepareSTSCredentials(ctx context.Context, roleArn, roleSe
|
||||
}
|
||||
|
||||
// Generate temporary credentials (deterministic based on sessionId)
|
||||
stsCredGen := sts.NewCredentialGenerator()
|
||||
stsCredGen := h.stsService.GetCredentialGenerator()
|
||||
stsCredsDet, err := stsCredGen.GenerateTemporaryCredentials(sessionId, expiration)
|
||||
if err != nil {
|
||||
return STSCredentials{}, nil, fmt.Errorf("failed to generate temporary credentials: %w", err)
|
||||
|
||||
@@ -31,6 +31,9 @@ type SeaweedFileIdClaims struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -44,6 +47,9 @@ type SeaweedFilerClaims struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,10 @@ func (fs *FilerServer) authenticateFilerJwt(r *http.Request, isWrite bool) (*sec
|
||||
glog.V(1).Infof("jwt claims not of type *SeaweedFilerClaims from %s", r.RemoteAddr)
|
||||
return nil, false
|
||||
}
|
||||
if claims.SessionId != "" {
|
||||
glog.V(1).Infof("jwt is an STS session token, not a filer credential, from %s", r.RemoteAddr)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if len(claims.AllowedMethods) > 0 {
|
||||
hasMethod := false
|
||||
|
||||
@@ -65,6 +65,9 @@ func (s *IamGrpcServer) checkAdminAuth(ctx context.Context) error {
|
||||
if err != nil || parsed == nil || !parsed.Valid {
|
||||
return status.Error(codes.Unauthenticated, "invalid admin token")
|
||||
}
|
||||
if claims, ok := parsed.Claims.(*security.SeaweedFilerAdminClaims); !ok || claims.SessionId != "" {
|
||||
return status.Error(codes.Unauthenticated, "invalid admin token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user