[security] rpc: Do not transfer access/secret key. (#4857)

This is an improvement upon existing implementation
by avoiding transfer of access and secret keys over
the network. This change only exchanges JWT tokens
generated by an rpc client. Even if the JWT can be
traced over the network on a non-TLS connection, this
change makes sure that we never really expose the
secret key over the network.
master
Harshavardhana 7 years ago committed by Dee Koder
parent f680b8482f
commit f8024cadbb
  1. 60
      cmd/admin-rpc-server_test.go
  2. 13
      cmd/auth-rpc-client.go
  3. 13
      cmd/auth-rpc-server.go
  4. 50
      cmd/auth-rpc-server_test.go
  5. 20
      cmd/browser-peer-rpc.go
  6. 21
      cmd/browser-peer-rpc_test.go
  7. 2
      cmd/browser-rpc-router.go
  8. 28
      cmd/jwt.go
  9. 53
      cmd/jwt_test.go
  10. 9
      cmd/lock-rpc-server_test.go
  11. 5
      cmd/notifier-config.go
  12. 10
      cmd/rpc-common.go
  13. 26
      cmd/web-handlers_test.go
  14. 7
      pkg/quick/quick.go

@ -33,16 +33,19 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
}
defer os.RemoveAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
adminServer := adminCmd{}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
err = adminServer.Login(&args, &LoginRPCReply{})
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
@ -52,7 +55,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
<-globalServiceSignalCh
}()
ga := AuthRPCArgs{AuthToken: reply.AuthToken}
ga := AuthRPCArgs{AuthToken: token}
genReply := AuthRPCReply{}
switch cmd {
case restartCmd:
@ -91,21 +94,25 @@ func TestReInitDisks(t *testing.T) {
// Setup admin rpc server for an XL backend.
globalIsXL = true
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
err = adminServer.Login(&args, &LoginRPCReply{})
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
}
authReply := AuthRPCReply{}
@ -114,12 +121,15 @@ func TestReInitDisks(t *testing.T) {
t.Errorf("Expected to pass, but failed with %v", err)
}
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
// Negative test case with admin rpc server setup for FS.
globalIsXL = false
fsAdminServer := adminCmd{}
fsArgs := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -130,7 +140,7 @@ func TestReInitDisks(t *testing.T) {
}
authArgs = AuthRPCArgs{
AuthToken: fsReply.AuthToken,
AuthToken: token,
}
authReply = AuthRPCReply{}
// Attempt ReInitDisks service on a FS backend.
@ -154,9 +164,14 @@ func TestGetConfig(t *testing.T) {
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -167,7 +182,7 @@ func TestGetConfig(t *testing.T) {
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
}
configReply := ConfigReply{}
@ -198,9 +213,12 @@ func TestWriteAndCommitConfig(t *testing.T) {
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -215,7 +233,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
tmpFileName := mustGetUUID()
wArgs := WriteConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
},
TmpFileName: tmpFileName,
Buf: buf,
@ -232,7 +250,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
cArgs := CommitConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: token,
},
FileName: tmpFileName,
}

@ -99,21 +99,22 @@ func (authClient *AuthRPCClient) Login() (err error) {
// Attempt to login if not logged in already.
if authClient.authToken == "" {
// Login to authenticate and acquire a new auth token.
authClient.authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
if err != nil {
return err
}
// Login to authenticate your token.
var (
loginMethod = authClient.config.serviceName + loginMethodName
loginArgs = LoginRPCArgs{
Username: authClient.config.accessKey,
Password: authClient.config.secretKey,
AuthToken: authClient.authToken,
Version: Version,
RequestTime: UTCNow(),
}
loginReply = LoginRPCReply{}
)
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &loginReply); err != nil {
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
return err
}
authClient.authToken = loginReply.AuthToken
}
return nil
}

@ -20,8 +20,7 @@ package cmd
const loginMethodName = ".Login"
// AuthRPCServer RPC server authenticates using JWT.
type AuthRPCServer struct {
}
type AuthRPCServer struct{}
// Login - Handles JWT based RPC login.
func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
@ -30,14 +29,10 @@ func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
return err
}
// Authenticate using JWT.
token, err := authenticateNode(args.Username, args.Password)
if err != nil {
return err
// Return an error if token is not valid.
if !isAuthTokenValid(args.AuthToken) {
return errAuthentication
}
// Return the token.
reply.AuthToken = token
return nil
}

@ -29,6 +29,10 @@ func TestLogin(t *testing.T) {
}
defer os.RemoveAll(rootPath)
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
ls := AuthRPCServer{}
testCases := []struct {
args LoginRPCArgs
@ -38,8 +42,7 @@ func TestLogin(t *testing.T) {
// Valid case.
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
},
skewTime: 0,
@ -48,8 +51,7 @@ func TestLogin(t *testing.T) {
// Valid username, password and request time, not version.
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: "INVALID-" + Version,
},
skewTime: 0,
@ -58,48 +60,16 @@ func TestLogin(t *testing.T) {
// Valid username, password and version, not request time
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
},
skewTime: 20 * time.Minute,
expectedErr: errServerTimeMismatch,
},
// Invalid username length
{
args: LoginRPCArgs{
Username: "aaa",
Password: "minio123",
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidAccessKeyLength,
},
// Invalid password length
{
args: LoginRPCArgs{
Username: "minio",
Password: "aaa",
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidSecretKeyLength,
},
// Invalid username
{
args: LoginRPCArgs{
Username: "aaaaa",
Password: creds.SecretKey,
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidAccessKeyID,
},
// Invalid password
// Invalid token, fails with authentication error
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: "aaaaaaaa",
AuthToken: "",
Version: Version,
},
skewTime: 0,
@ -108,7 +78,7 @@ func TestLogin(t *testing.T) {
}
for i, test := range testCases {
reply := LoginRPCReply{}
test.args.RequestTime = time.Now().Add(test.skewTime).UTC()
test.args.RequestTime = UTCNow().Add(test.skewTime)
err := ls.Login(&test.args, &reply)
if err != test.expectedErr {
t.Errorf("Test %d: Expected error %v but received %v",

@ -23,26 +23,6 @@ import (
"time"
)
// Login handler implements JWT login token generator, which upon login request
// along with username and password is generated.
func (br *browserPeerAPIHandlers) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
// Validate LoginRPCArgs
if err := args.IsValid(); err != nil {
return err
}
// Authenticate using JWT.
token, err := authenticateWeb(args.Username, args.Password)
if err != nil {
return err
}
// Return the token.
reply.AuthToken = token
return nil
}
// SetAuthPeerArgs - Arguments collection for SetAuth RPC call
type SetAuthPeerArgs struct {
// For Auth

@ -91,9 +91,12 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
// Validate for failure in login handler with previous credentials.
rclient = newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
defer rclient.Close()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
rargs := &LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -105,20 +108,18 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
}
}
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
// Validate for success in loing handled with valid credetnails.
rargs = &LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
rreply = &LoginRPCReply{}
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
if err != nil {
if err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply); err != nil {
t.Fatal(err)
}
// Validate all the replied fields after successful login.
if rreply.AuthToken == "" {
t.Fatalf("Generated token cannot be empty %s", errInvalidToken)
}
}

@ -35,7 +35,7 @@ type browserPeerAPIHandlers struct {
// Register RPC router
func registerBrowserPeerRPCRouter(mux *router.Router) error {
bpHandlers := &browserPeerAPIHandlers{}
bpHandlers := &browserPeerAPIHandlers{AuthRPCServer{}}
bpRPCServer := newRPCServer()
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)

@ -63,10 +63,10 @@ func authenticateJWT(accessKey, secretKey string, expiry time.Duration) (string,
}
utcNow := UTCNow()
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims{
"exp": utcNow.Add(expiry).Unix(),
"iat": utcNow.Unix(),
"sub": accessKey,
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
ExpiresAt: utcNow.Add(expiry).Unix(),
IssuedAt: utcNow.Unix(),
Subject: accessKey,
})
return token.SignedString([]byte(serverCred.SecretKey))
@ -93,13 +93,17 @@ func keyFuncCallback(jwtToken *jwtgo.Token) (interface{}, error) {
}
func isAuthTokenValid(tokenString string) bool {
jwtToken, err := jwtgo.Parse(tokenString, keyFuncCallback)
var claims jwtgo.StandardClaims
jwtToken, err := jwtgo.ParseWithClaims(tokenString, &claims, keyFuncCallback)
if err != nil {
errorIf(err, "Unable to parse JWT token string")
return false
}
return jwtToken.Valid
if err = claims.Valid(); err != nil {
errorIf(err, "Invalid claims in JWT token string")
return false
}
return jwtToken.Valid && claims.Subject == serverConfig.GetCredential().AccessKey
}
func isHTTPRequestValid(req *http.Request) bool {
@ -110,14 +114,20 @@ func isHTTPRequestValid(req *http.Request) bool {
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors.
func webRequestAuthenticate(req *http.Request) error {
jwtToken, err := jwtreq.ParseFromRequest(req, jwtreq.AuthorizationHeaderExtractor, keyFuncCallback)
var claims jwtgo.StandardClaims
jwtToken, err := jwtreq.ParseFromRequestWithClaims(req, jwtreq.AuthorizationHeaderExtractor, &claims, keyFuncCallback)
if err != nil {
if err == jwtreq.ErrNoTokenInRequest {
return errNoAuthToken
}
return errAuthentication
}
if err = claims.Valid(); err != nil {
return err
}
if claims.Subject != serverConfig.GetCredential().AccessKey {
return errInvalidAccessKeyID
}
if !jwtToken.Valid {
return errAuthentication
}

@ -17,6 +17,7 @@
package cmd
import (
"net/http"
"os"
"testing"
)
@ -88,6 +89,58 @@ func TestAuthenticateURL(t *testing.T) {
testAuthenticate("url", t)
}
// Tests web request authenticator.
func TestWebRequestAuthenticate(t *testing.T) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer os.RemoveAll(testPath)
creds := serverConfig.GetCredential()
token, err := getTokenString(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatalf("unable get token %s", err)
}
testCases := []struct {
req *http.Request
expectedErr error
}{
// Set valid authorization header.
{
req: &http.Request{
Header: http.Header{
"Authorization": []string{token},
},
},
expectedErr: nil,
},
// No authorization header.
{
req: &http.Request{
Header: http.Header{},
},
expectedErr: errNoAuthToken,
},
// Invalid authorization token.
{
req: &http.Request{
Header: http.Header{
"Authorization": []string{"invalid-token"},
},
},
expectedErr: errAuthentication,
},
}
for i, testCase := range testCases {
gotErr := webRequestAuthenticate(testCase.req)
if testCase.expectedErr != gotErr {
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
}
}
}
func BenchmarkAuthenticateNode(b *testing.B) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {

@ -58,9 +58,12 @@ func createLockTestServer(t *testing.T) (string, *lockServer, string) {
},
}
creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
loginArgs := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
AuthToken: token,
Version: Version,
RequestTime: UTCNow(),
}
@ -69,8 +72,6 @@ func createLockTestServer(t *testing.T) (string, *lockServer, string) {
if err != nil {
t.Fatalf("Failed to login to lock server - %v", err)
}
token := loginReply.AuthToken
return testPath, locker, token
}

@ -235,10 +235,7 @@ func (n *notifier) Validate() error {
if err := n.MySQL.Validate(); err != nil {
return err
}
if err := n.MQTT.Validate(); err != nil {
return err
}
return nil
return n.MQTT.Validate()
}
func (n *notifier) SetAMQPByID(accountID string, amqpn amqpNotify) {

@ -60,8 +60,7 @@ type AuthRPCReply struct{}
// LoginRPCArgs - login username and password for RPC.
type LoginRPCArgs struct {
Username string
Password string
AuthToken string
Version string
RequestTime time.Time
}
@ -80,11 +79,8 @@ func (args LoginRPCArgs) IsValid() error {
return nil
}
// LoginRPCReply - login reply provides generated token to be used
// with subsequent requests.
type LoginRPCReply struct {
AuthToken string
}
// LoginRPCReply - login reply is a dummy struct perhaps for future use.
type LoginRPCReply struct{}
// LockArgs represents arguments for any authenticated lock RPC call.
type LockArgs struct {

@ -34,6 +34,7 @@ import (
"strings"
"testing"
jwtgo "github.com/dgrijalva/jwt-go"
humanize "github.com/dustin/go-humanize"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
@ -667,6 +668,16 @@ func TestWebCreateURLToken(t *testing.T) {
ExecObjectLayerTest(t, testCreateURLToken)
}
func getTokenString(accessKey, secretKey string) (string, error) {
utcNow := UTCNow()
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
ExpiresAt: utcNow.Add(defaultJWTExpiry).Unix(),
IssuedAt: utcNow.Unix(),
Subject: accessKey,
})
return token.SignedString([]byte(secretKey))
}
func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler) {
apiRouter := initTestWebRPCEndPoint(obj)
credentials := serverConfig.GetCredential()
@ -700,6 +711,21 @@ func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler)
if !isAuthTokenValid(tokenReply.Token) {
t.Fatalf("token is not valid")
}
// Token is invalid.
if isAuthTokenValid("") {
t.Fatalf("token shouldn't be valid, but it is")
}
token, err := getTokenString("invalid-access", credentials.SecretKey)
if err != nil {
t.Fatal(err)
}
// Token has invalid access key.
if isAuthTokenValid(token) {
t.Fatalf("token shouldn't be valid, but it is")
}
}
// Wrapper for calling Upload Handler

@ -92,12 +92,7 @@ func (d config) Save(filename string) error {
func (d config) Load(filename string) error {
d.lock.Lock()
defer d.lock.Unlock()
if err := loadFileConfig(filename, d.data); err != nil {
return err
}
return nil
return loadFileConfig(filename, d.data)
}
// Data - grab internal data map for reading

Loading…
Cancel
Save