From db387912f2b02aa775c8b258147490046320d2c7 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Wed, 27 Jan 2016 01:52:54 -0800 Subject: [PATCH] jwt: Deprecate RSA usage, use HMAC instead. HMAC is a much simpler implementation, providing the same benefits as RSA, avoids additional steps and keeps the code simpler. This patch also additionally - Implements PutObjectURL API. - GetObjectURL, PutObjectURL take TargetHost as another argument for generating URL's for proper target destination. - Adds experimental TLS support for JSON RPC calls. --- Browser.md | 39 +++++++++++++++++++++ JWT.md | 61 -------------------------------- jwt-auth-handler.go | 8 ++--- jwt.go | 71 +++++++++++-------------------------- pkg/fs/fs-multipart.go | 4 +-- routers.go | 29 ++++++++++++---- web-config.go | 64 ---------------------------------- web-definitions.go | 10 +++++- web-handlers.go | 79 +++++++++++++++++++++++++++++++++--------- 9 files changed, 160 insertions(+), 205 deletions(-) create mode 100644 Browser.md delete mode 100644 JWT.md delete mode 100644 web-config.go diff --git a/Browser.md b/Browser.md new file mode 100644 index 000000000..b12dbea87 --- /dev/null +++ b/Browser.md @@ -0,0 +1,39 @@ +## Minio Browser + +Minio Browser uses Json Web Tokens to authenticate JSON RPC requests. + +Initial request generates a token for 'AccessKey' and 'SecretKey' +provided by the user. + +
+Currently these tokens expire after 10hrs, this is not configurable yet. +
+ +### Start minio server + +``` +minio server +``` + +### JSON RPC APIs. + +JSON RPC namespace is `Web`. + +#### Auth Operations + +* Login - waits for 'username, password' and on success replies a new Json Web Token (JWT). +* ResetToken - resets token, requires password and token. +* Logout - currently a dummy operation. + +#### Bucket/Object Operations. + +* ListBuckets - lists buckets, requires a valid token. +* ListObjects - lists objects, requires a valid token. +* MakeBucket - make a new bucket, requires a valid token. +* GetObjectURL - generates a URL for download access, requires a valid token. + (generated URL is valid for 1hr) +* PutObjectURL - generates a URL for upload access, requies a valid token. + (generated URL is valid for 1hr) + +#### Server Operations. +* DiskInfo - get backend disk statistics. diff --git a/JWT.md b/JWT.md deleted file mode 100644 index f5f1882b7..000000000 --- a/JWT.md +++ /dev/null @@ -1,61 +0,0 @@ -### Generate RSA keys for JWT - -``` -mkdir -p ~/.minio/web -``` - -``` -openssl genrsa -out ~/.minio/web/private.key 2048 -``` - -``` -openssl rsa -in ~/.minio/web/private.key -outform PEM -pubout -out ~/.minio/web/public.key -``` -### Start minio server - -``` -minio server -``` - -### Implemented JSON RPC APIs. - -Namespace `Web` - -* Login - waits for 'username, password' and on success replies a new JWT token. -* ResetToken - resets token, requires password and token. -* Logout - currently a dummy operation. -* ListBuckets - lists buckets, requires valid token. -* ListObjects - lists objects, requires valid token. -* GetObjectURL - generates a url for download access, requires valid token. - -### Now you can use `webrpc.js` to make requests. - -- Login example -```js -var webRPC = require('webrpc'); -var web = new webRPC("http://localhost:9001/rpc") - -// Generate JWT Token. -web.Login({"username": "YOUR-ACCESS-KEY-ID", "password": "YOUR-SECRET-ACCESS-KEY"}) - .then(function(data) { - console.log("success : ", data); - }) - .catch(function(error) { - console.log("fail : ", error.toString()); - }); -``` - -- ListBuckets example -```js -var webRPC = require('webrpc'); -var web = new webRPC("http://localhost:9001/rpc", "my-token") - -// Generate Token. -web.ListBuckets() - .then(function(data) { - console.log("Success : ", data); - }) - .catch(function(error) { - console.log("fail : ", error.toString()); - }); -``` diff --git a/jwt-auth-handler.go b/jwt-auth-handler.go index f89ad19a4..a944b187c 100644 --- a/jwt-auth-handler.go +++ b/jwt-auth-handler.go @@ -43,13 +43,13 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Validate Authorization header to be valid. jwt := InitJWT() - token, err := jwtgo.ParseFromRequest(r, func(token *jwtgo.Token) (interface{}, error) { - if _, ok := token.Method.(*jwtgo.SigningMethodRSA); !ok { + token, e := jwtgo.ParseFromRequest(r, func(token *jwtgo.Token) (interface{}, error) { + if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } - return jwt.PublicKey, nil + return jwt.secretAccessKey, nil }) - if err != nil || !token.Valid { + if e != nil || !token.Valid { w.WriteHeader(http.StatusUnauthorized) return } diff --git a/jwt.go b/jwt.go index 492cdd3cb..17ac2445a 100644 --- a/jwt.go +++ b/jwt.go @@ -17,11 +17,7 @@ package main import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" - "io/ioutil" + "bytes" "time" jwtgo "github.com/dgrijalva/jwt-go" @@ -31,72 +27,47 @@ import ( // JWT - jwt auth backend type JWT struct { - // Public value. - PublicKey *rsa.PublicKey - // private values. - privateKey *rsa.PrivateKey - accessKeyID string - secretAccessKey string + accessKeyID []byte + secretAccessKey []byte } +// Default - each token expires in 10hrs. const ( - jwtExpirationDelta = 10 + tokenExpires time.Duration = 10 ) // InitJWT - initialize. func InitJWT() *JWT { - jwt := &JWT{ - privateKey: getPrivateKey(), - } - // Validate if public key is of algorithm *rsa.PublicKey. - var ok bool - jwt.PublicKey, ok = jwt.privateKey.Public().(*rsa.PublicKey) - if !ok { - fatalIf(probe.NewError(errors.New("")), "Unsupported type of public key algorithm found.", nil) - } - // Load credentials configuration. + jwt := &JWT{} + // Load credentials. config, err := loadConfigV2() fatalIf(err.Trace("JWT"), "Unable to load configuration file.", nil) // Save access, secret keys. - jwt.accessKeyID = config.Credentials.AccessKeyID - jwt.secretAccessKey = config.Credentials.SecretAccessKey + jwt.accessKeyID = []byte(config.Credentials.AccessKeyID) + jwt.secretAccessKey = []byte(config.Credentials.SecretAccessKey) return jwt } // GenerateToken - generates a new Json Web Token based on the incoming user id. -func (b *JWT) GenerateToken(userName string) (string, error) { - token := jwtgo.New(jwtgo.SigningMethodRS512) +func (jwt *JWT) GenerateToken(userName string) (string, *probe.Error) { + token := jwtgo.New(jwtgo.SigningMethodHS512) // Token expires in 10hrs. - token.Claims["exp"] = time.Now().Add(time.Hour * time.Duration(jwtExpirationDelta)).Unix() + token.Claims["exp"] = time.Now().Add(time.Hour * tokenExpires).Unix() token.Claims["iat"] = time.Now().Unix() token.Claims["sub"] = userName - tokenString, err := token.SignedString(b.privateKey) - if err != nil { - return "", err + tokenString, e := token.SignedString(jwt.secretAccessKey) + if e != nil { + return "", probe.NewError(e) } return tokenString, nil } -// Authenticate - authenticates the username and password. -func (b *JWT) Authenticate(username, password string) bool { - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(b.secretAccessKey), 10) - if username == b.accessKeyID { - return bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) == nil - } - return false -} - -// getPrivateKey - get the generated private key. -func getPrivateKey() *rsa.PrivateKey { - pemBytes, err := ioutil.ReadFile(mustGetPrivateKeyPath()) - if err != nil { - panic(err) - } - data, _ := pem.Decode([]byte(pemBytes)) - privateKeyImported, err := x509.ParsePKCS1PrivateKey(data.Bytes) - if err != nil { - panic(err) +// Authenticate - authenticates incoming username and password. +func (jwt *JWT) Authenticate(userName, password string) bool { + if !bytes.Equal([]byte(userName), jwt.accessKeyID) { + return false } - return privateKeyImported + hashedPassword, _ := bcrypt.GenerateFromPassword(jwt.secretAccessKey, bcrypt.DefaultCost) + return bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) == nil } diff --git a/pkg/fs/fs-multipart.go b/pkg/fs/fs-multipart.go index e37a0e0bc..2c2372414 100644 --- a/pkg/fs/fs-multipart.go +++ b/pkg/fs/fs-multipart.go @@ -162,7 +162,7 @@ func (fs Filesystem) NewMultipartUpload(bucket, object string) (string, *probe.E bucket = fs.denormalizeBucket(bucket) bucketPath := filepath.Join(fs.path, bucket) - if _, e := os.Stat(bucketPath); e != nil { + if _, e = os.Stat(bucketPath); e != nil { // check bucket exists if os.IsNotExist(e) { return "", probe.NewError(BucketNotFound{Bucket: bucket}) @@ -172,7 +172,7 @@ func (fs Filesystem) NewMultipartUpload(bucket, object string) (string, *probe.E objectPath := filepath.Join(bucketPath, object) objectDir := filepath.Dir(objectPath) - if _, e := os.Stat(objectDir); e != nil { + if _, e = os.Stat(objectDir); e != nil { if !os.IsNotExist(e) { return "", probe.NewError(e) } diff --git a/routers.go b/routers.go index 67bcf4e23..80d03755a 100644 --- a/routers.go +++ b/routers.go @@ -46,12 +46,20 @@ type WebAPI struct { AccessLog bool // Minio client instance. Client minio.CloudStorageClient + + // private params. + inSecure bool // Enabled if TLS is false. + apiAddress string // api destination address. + // accessKeys kept to be used internally. + accessKeyID string + secretAccessKey string } func getWebAPIHandler(web *WebAPI) http.Handler { var mwHandlers = []MiddlewareHandler{ TimeValidityHandler, // Validate time. CorsHandler, // CORS added only for testing purposes. + AuthHandler, // Authentication handler for verifying tokens. } if web.AccessLog { mwHandlers = append(mwHandlers, AccessLogHandler) @@ -106,20 +114,27 @@ func registerCloudStorageAPI(mux *router.Router, a CloudStorageAPI) { // getNewWebAPI instantiate a new WebAPI. func getNewWebAPI(conf cloudServerConfig) *WebAPI { // Split host port. - _, port, e := net.SplitHostPort(conf.Address) + host, port, e := net.SplitHostPort(conf.Address) fatalIf(probe.NewError(e), "Unable to parse web addess.", nil) - // Default host to 'localhost'. - host := "localhost" + // Default host is 'localhost', if no host present. + if host == "" { + host = "localhost" + } // Initialize minio client for AWS Signature Version '4' - client, e := minio.NewV4(net.JoinHostPort(host, port), conf.AccessKeyID, conf.SecretAccessKey, true) + inSecure := !conf.TLS // Insecure true when TLS is false. + client, e := minio.NewV4(net.JoinHostPort(host, port), conf.AccessKeyID, conf.SecretAccessKey, inSecure) fatalIf(probe.NewError(e), "Unable to initialize minio client", nil) web := &WebAPI{ - FSPath: conf.Path, - AccessLog: conf.AccessLog, - Client: client, + FSPath: conf.Path, + AccessLog: conf.AccessLog, + Client: client, + inSecure: inSecure, + apiAddress: conf.Address, + accessKeyID: conf.AccessKeyID, + secretAccessKey: conf.SecretAccessKey, } return web } diff --git a/web-config.go b/web-config.go deleted file mode 100644 index a5df87eb0..000000000 --- a/web-config.go +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Minio Cloud Storage, (C) 2016 Minio, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package main - -import ( - "os" - "path/filepath" - - "github.com/minio/minio-xl/pkg/probe" - "github.com/minio/minio/pkg/user" -) - -var customWebConfigDir = "" - -// getWebConfigDir get web config dir. -func getWebConfigDir() (string, *probe.Error) { - if customWebConfigDir != "" { - return customWebConfigDir, nil - } - homeDir, e := user.HomeDir() - if e != nil { - return "", probe.NewError(e) - } - webConfigDir := filepath.Join(homeDir, ".minio", "web") - return webConfigDir, nil -} - -func mustGetWebConfigDir() string { - webConfigDir, err := getWebConfigDir() - fatalIf(err.Trace(), "Unable to get config path.", nil) - return webConfigDir -} - -// createWebConfigDir create users config path -func createWebConfigDir() *probe.Error { - webConfigDir, err := getWebConfigDir() - if err != nil { - return err.Trace() - } - if err := os.MkdirAll(webConfigDir, 0700); err != nil { - return probe.NewError(err) - } - return nil -} - -func mustGetPrivateKeyPath() string { - webConfigDir, err := getWebConfigDir() - fatalIf(err.Trace(), "Unable to get config path.", nil) - return webConfigDir + "/private.key" -} diff --git a/web-definitions.go b/web-definitions.go index 1e183b67f..0a413e3dd 100644 --- a/web-definitions.go +++ b/web-definitions.go @@ -53,8 +53,16 @@ type ObjectInfo struct { Size int64 `json:"size"` } -// GetObjectURLArgs - get object url. +// PutObjectURLArgs - args to generate url for upload access. +type PutObjectURLArgs struct { + TargetHost string `json:"targetHost"` + BucketName string `json:"bucketName"` + ObjectName string `json:"objectName"` +} + +// GetObjectURLArgs - args to generate url for download access. type GetObjectURLArgs struct { + TargetHost string `json:"targetHost"` BucketName string `json:"bucketName"` ObjectName string `json:"objectName"` } diff --git a/web-handlers.go b/web-handlers.go index b5f89ddba..d09068d9a 100644 --- a/web-handlers.go +++ b/web-handlers.go @@ -18,10 +18,13 @@ package main import ( "fmt" + "net" "net/http" "time" jwtgo "github.com/dgrijalva/jwt-go" + "github.com/minio/minio-go" + "github.com/minio/minio-xl/pkg/probe" "github.com/minio/minio/pkg/disk" ) @@ -29,13 +32,13 @@ import ( // authenticated request. func isAuthenticated(req *http.Request) bool { jwt := InitJWT() - tokenRequest, err := jwtgo.ParseFromRequest(req, func(token *jwtgo.Token) (interface{}, error) { - if _, ok := token.Method.(*jwtgo.SigningMethodRSA); !ok { + tokenRequest, e := jwtgo.ParseFromRequest(req, func(token *jwtgo.Token) (interface{}, error) { + if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } - return jwt.PublicKey, nil + return jwt.secretAccessKey, nil }) - if err != nil { + if e != nil { return false } return tokenRequest.Valid @@ -46,9 +49,9 @@ func (web *WebAPI) DiskInfo(r *http.Request, args *DiskInfoArgs, reply *disk.Inf if !isAuthenticated(r) { return errUnAuthorizedRequest } - info, err := disk.GetInfo(web.FSPath) - if err != nil { - return err + info, e := disk.GetInfo(web.FSPath) + if e != nil { + return e } *reply = info return nil @@ -67,9 +70,9 @@ func (web *WebAPI) ListBuckets(r *http.Request, args *ListBucketsArgs, reply *[] if !isAuthenticated(r) { return errUnAuthorizedRequest } - buckets, err := web.Client.ListBuckets() - if err != nil { - return err + buckets, e := web.Client.ListBuckets() + if e != nil { + return e } for _, bucket := range buckets { *reply = append(*reply, BucketInfo{ @@ -101,16 +104,60 @@ func (web *WebAPI) ListObjects(r *http.Request, args *ListObjectsArgs, reply *[] return nil } -// GetObjectURL - get object url. +func getTargetHost(apiAddress, targetHost string) (string, *probe.Error) { + if targetHost != "" { + _, port, e := net.SplitHostPort(apiAddress) + if e != nil { + return "", probe.NewError(e) + } + host, _, e := net.SplitHostPort(targetHost) + if e != nil { + return "", probe.NewError(e) + } + targetHost = net.JoinHostPort(host, port) + } + return targetHost, nil +} + +// PutObjectURL - generates url for upload access. +func (web *WebAPI) PutObjectURL(r *http.Request, args *PutObjectURLArgs, reply *string) error { + if !isAuthenticated(r) { + return errUnAuthorizedRequest + } + targetHost, err := getTargetHost(web.apiAddress, args.TargetHost) + if err != nil { + return probe.WrapError(err) + } + client, e := minio.NewV4(targetHost, web.accessKeyID, web.secretAccessKey, web.inSecure) + if e != nil { + return e + } + signedURLStr, e := client.PresignedPutObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second) + if e != nil { + return e + } + *reply = signedURLStr + return nil +} + +// GetObjectURL - generates url for download access. func (web *WebAPI) GetObjectURL(r *http.Request, args *GetObjectURLArgs, reply *string) error { if !isAuthenticated(r) { return errUnAuthorizedRequest } - urlStr, err := web.Client.PresignedGetObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second) + targetHost, err := getTargetHost(web.apiAddress, args.TargetHost) if err != nil { - return err + return probe.WrapError(err) + } + client, e := minio.NewV4(targetHost, web.accessKeyID, web.secretAccessKey, web.inSecure) + if e != nil { + return e + } + signedURLStr, e := client.PresignedGetObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second) + if e != nil { + return e } - *reply = urlStr + *reply = signedURLStr return nil } @@ -120,7 +167,7 @@ func (web *WebAPI) Login(r *http.Request, args *LoginArgs, reply *AuthToken) err if jwt.Authenticate(args.Username, args.Password) { token, err := jwt.GenerateToken(args.Username) if err != nil { - return err + return probe.WrapError(err.Trace()) } reply.Token = token return nil @@ -134,7 +181,7 @@ func (web *WebAPI) RefreshToken(r *http.Request, args *LoginArgs, reply *AuthTok jwt := InitJWT() token, err := jwt.GenerateToken(args.Username) if err != nil { - return err + return probe.WrapError(err.Trace()) } reply.Token = token return nil