Skip to content

Commit e1e91b4

Browse files
committed
STS integration, JWT auth and Stateless MCS
This commit changes the authentication mechanism between mcs and minio to an sts (security token service) schema using the user provided credentials, previously mcs was using master credentials. With that said in order for you to login to MCS as an admin your user must exists first on minio and have enough privileges to do administrative operations. ``` ./mc admin user add myminio alevsk alevsk12345 ``` ``` cat admin.json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "admin:*", "s3:*" ], "Resource": [ "arn:aws:s3:::*" ] } ] } ./mc admin policy add myminio admin admin.json ``` ``` ./mc admin policy set myminio admin user=alevsk ```
1 parent 605b800 commit e1e91b4

29 files changed

+914
-301
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ assets:
2222

2323
test:
2424
@(go test -race -v github.com/minio/mcs/restapi/...)
25+
@(go test -race -v github.com/minio/mcs/pkg/auth)
2526

2627
coverage:
2728
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ $ mc admin policy set myminio mcsAdmin user=mcs
5454
To run the server:
5555

5656
```
57+
export MCS_HMAC_JWT_SECRET=YOURJWTSIGNINGSECRET
58+
59+
#required to encrypt jwet payload
60+
export MCS_PBKDF_PASSPHRASE=SECRET
61+
62+
#required to encrypt jwet payload
63+
export MCS_PBKDF_SALT=SECRET
64+
5765
export MCS_ACCESS_KEY=mcs
5866
export MCS_SECRET_KEY=YOURMCSSECRET
5967
export MCS_MINIO_SERVER=http://localhost:9000

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/minio/mcs
33
go 1.14
44

55
require (
6+
github.com/dgrijalva/jwt-go v3.2.0+incompatible
67
github.com/elazarl/go-bindata-assetfs v1.0.0
78
github.com/go-openapi/errors v0.19.4
89
github.com/go-openapi/loads v0.19.5
@@ -12,11 +13,14 @@ require (
1213
github.com/go-openapi/swag v0.19.8
1314
github.com/go-openapi/validate v0.19.7
1415
github.com/jessevdk/go-flags v1.4.0
16+
github.com/json-iterator/go v1.1.9
1517
github.com/minio/cli v1.22.0
1618
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
1719
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab
1820
github.com/minio/minio-go/v6 v6.0.53
21+
github.com/satori/go.uuid v1.2.0
1922
github.com/stretchr/testify v1.5.1
2023
github.com/unrolled/secure v1.0.7
24+
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
2125
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
2226
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab h1:9hlqghJl3e3HorXa6AD
390390
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
391391
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
392392
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
393+
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
393394
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
394395
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
395396
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
@@ -494,6 +495,7 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
494495
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
495496
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
496497
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
498+
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
497499
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
498500
github.com/secure-io/sio-go v0.3.0 h1:QKGb6rGJeiExac9wSWxnWPYo8O8OFN7lxXQvHshX6vo=
499501
github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU=

pkg/auth/jwt.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2020 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package auth
18+
19+
import (
20+
"crypto/aes"
21+
"crypto/cipher"
22+
"crypto/rand"
23+
"crypto/sha1"
24+
"encoding/base64"
25+
"errors"
26+
"fmt"
27+
"io"
28+
"log"
29+
"strings"
30+
31+
jwtgo "github.com/dgrijalva/jwt-go"
32+
xjwt "github.com/minio/mcs/pkg/auth/jwt"
33+
"github.com/minio/minio-go/v6/pkg/credentials"
34+
"github.com/minio/minio/cmd"
35+
uuid "github.com/satori/go.uuid"
36+
"golang.org/x/crypto/pbkdf2"
37+
)
38+
39+
var (
40+
errAuthentication = errors.New("Authentication failed, check your access credentials")
41+
errNoAuthToken = errors.New("JWT token missing")
42+
errReadingToken = errors.New("JWT internal data is malformed")
43+
errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
44+
)
45+
46+
// derivedKey is the key used to encrypt the JWT claims, its derived using pbkdf on MCS_PBKDF_PASSPHRASE with MCS_PBKDF_SALT
47+
var derivedKey = pbkdf2.Key([]byte(xjwt.GetPBKDFPassphrase()), []byte(xjwt.GetPBKDFSalt()), 4096, 32, sha1.New)
48+
49+
// IsJWTValid returns true or false depending if the provided jwt is valid or not
50+
func IsJWTValid(token string) bool {
51+
_, err := JWTAuthenticate(token)
52+
return err == nil
53+
}
54+
55+
type DecryptedClaims struct {
56+
AccessKeyID string
57+
SecretAccessKey string
58+
SessionToken string
59+
}
60+
61+
// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
62+
// if the jwt claims.Data is valid we proceed to decrypt the information inside
63+
//
64+
// returns claims after validation in the following format:
65+
//
66+
// type DecryptedClaims struct {
67+
// AccessKeyID
68+
// SecretAccessKey
69+
// SessionToken
70+
// }
71+
func JWTAuthenticate(token string) (*DecryptedClaims, error) {
72+
if token == "" {
73+
return nil, errNoAuthToken
74+
}
75+
// initialize claims object
76+
claims := xjwt.NewMapClaims()
77+
// populate the claims object
78+
if err := xjwt.ParseWithClaims(token, claims); err != nil {
79+
return nil, errAuthentication
80+
}
81+
// decrypt the claims.Data field
82+
claimTokens, err := decryptClaims(claims.Data)
83+
if err != nil {
84+
// we print decryption token error information for debugging purposes
85+
log.Println(err)
86+
// we return a generic error that doesn't give any information to attackers
87+
return nil, errReadingToken
88+
}
89+
// claimsTokens contains the decrypted STS claims
90+
return claimTokens, nil
91+
}
92+
93+
// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials
94+
// NewJWTWithClaimsForClient first encrypt the clients and the sign them
95+
func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
96+
if credentials != nil {
97+
encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
98+
if err != nil {
99+
return "", err
100+
}
101+
claims := xjwt.NewStandardClaims()
102+
claims.SetExpiry(cmd.UTCNow().Add(xjwt.GetMcsSTSAndJWTDurationTime()))
103+
claims.SetSubject(uuid.NewV4().String())
104+
claims.SetData(encryptedClaims)
105+
claims.SetAudience(audience)
106+
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
107+
return jwt.SignedString([]byte(xjwt.GetHmacJWTSecret()))
108+
}
109+
return "", errors.New("provided credentials are empty")
110+
}
111+
112+
// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
113+
// returns a base64 encoded ciphertext
114+
func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
115+
payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
116+
ciphertext, err := encrypt(payload)
117+
if err != nil {
118+
return "", err
119+
}
120+
return base64.StdEncoding.EncodeToString(ciphertext), nil
121+
}
122+
123+
// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *DecryptedClaims object
124+
func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
125+
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
126+
if err != nil {
127+
log.Println(err)
128+
return nil, errClaimsFormat
129+
}
130+
plaintext, err := decrypt(decoded)
131+
if err != nil {
132+
log.Println(err)
133+
return nil, errClaimsFormat
134+
}
135+
s := strings.Split(string(plaintext), ":")
136+
// Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
137+
if len(s) != 3 {
138+
return nil, errClaimsFormat
139+
}
140+
accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
141+
return &DecryptedClaims{
142+
AccessKeyID: accessKeyID,
143+
SecretAccessKey: secretAccessKey,
144+
SessionToken: sessionToken,
145+
}, nil
146+
}
147+
148+
// Encrypt a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
149+
func encrypt(plaintext []byte) ([]byte, error) {
150+
block, _ := aes.NewCipher(derivedKey)
151+
gcm, err := cipher.NewGCM(block)
152+
if err != nil {
153+
return nil, err
154+
}
155+
nonce := make([]byte, gcm.NonceSize())
156+
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
157+
return nil, err
158+
}
159+
cipherText := gcm.Seal(nonce, nonce, plaintext, nil)
160+
return cipherText, nil
161+
}
162+
163+
// Decrypts a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
164+
func decrypt(data []byte) ([]byte, error) {
165+
block, err := aes.NewCipher(derivedKey)
166+
if err != nil {
167+
return nil, err
168+
}
169+
gcm, err := cipher.NewGCM(block)
170+
if err != nil {
171+
return nil, err
172+
}
173+
nonceSize := gcm.NonceSize()
174+
nonce, cipherText := data[:nonceSize], data[nonceSize:]
175+
plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
176+
if err != nil {
177+
return nil, err
178+
}
179+
return plaintext, nil
180+
}

restapi/sessions/sessions.go renamed to pkg/auth/jwt/config.go

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,58 +14,18 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
package sessions
17+
package jwt
1818

1919
import (
2020
"crypto/rand"
2121
"io"
22+
"strconv"
2223
"strings"
23-
"sync"
24+
"time"
2425

25-
mcCmd "github.com/minio/mc/cmd"
26+
"github.com/minio/minio/pkg/env"
2627
)
2728

28-
type Singleton struct {
29-
sessions map[string]*mcCmd.Config
30-
}
31-
32-
var instance *Singleton
33-
var once sync.Once
34-
35-
// Returns a Singleton instance that keeps the sessions
36-
func GetInstance() *Singleton {
37-
once.Do(func() {
38-
//build sessions hash
39-
sessions := make(map[string]*mcCmd.Config)
40-
41-
instance = &Singleton{
42-
sessions: sessions,
43-
}
44-
})
45-
return instance
46-
}
47-
48-
// The delete built-in function deletes the element with the specified key (m[key]) from the map.
49-
// If m is nil or there is no such element, delete is a no-op. https://golang.org/pkg/builtin/#delete
50-
func (s *Singleton) DeleteSession(sessionID string) {
51-
delete(s.sessions, sessionID)
52-
}
53-
54-
func (s *Singleton) NewSession(cfg *mcCmd.Config) string {
55-
// genereate random session id
56-
sessionID := RandomCharString(64)
57-
// store the cfg under that session id
58-
s.sessions[sessionID] = cfg
59-
return sessionID
60-
}
61-
62-
func (s *Singleton) ValidSession(sessionID string) bool {
63-
if _, ok := s.sessions[sessionID]; ok {
64-
return true
65-
}
66-
return false
67-
}
68-
6929
// Do not use:
7030
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
7131
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
@@ -93,3 +53,42 @@ func RandomCharString(n int) string {
9353
}
9454
return s.String()
9555
}
56+
57+
// defaultHmacJWTPassphrase will be used by default if application is not configured with a custom MCS_HMAC_JWT_SECRET secret
58+
var defaultHmacJWTPassphrase = RandomCharString(64)
59+
60+
// GetHmacJWTSecret returns the 64 bytes secret used for signing the generated JWT for the application
61+
func GetHmacJWTSecret() string {
62+
return env.Get(McsHmacJWTSecret, defaultHmacJWTPassphrase)
63+
}
64+
65+
// McsSTSAndJWTDurationSeconds returns the default session duration for the STS requested tokens and the generated JWTs.
66+
// Ideally both values should match so jwt and Minio sts sessions expires at the same time.
67+
func GetMcsSTSAndJWTDurationInSeconds() int {
68+
duration, err := strconv.Atoi(env.Get(McsSTSAndJWTDurationSeconds, "3600"))
69+
if err != nil {
70+
duration = 3600
71+
}
72+
return duration
73+
}
74+
75+
// GetMcsSTSAndJWTDurationTime returns GetMcsSTSAndJWTDurationInSeconds in duration format
76+
func GetMcsSTSAndJWTDurationTime() time.Duration {
77+
duration := GetMcsSTSAndJWTDurationInSeconds()
78+
return time.Duration(duration) * time.Second
79+
}
80+
81+
// defaultPBKDFPassphrase
82+
var defaultPBKDFPassphrase = RandomCharString(64)
83+
84+
// GetPBKDFPassphrase returns passphrase for the pbkdf2 function used to encrypt JWT payload
85+
func GetPBKDFPassphrase() string {
86+
return env.Get(McsPBKDFPassphrase, defaultPBKDFPassphrase)
87+
}
88+
89+
var defaultPBKDFSalt = RandomCharString(64)
90+
91+
// GetPBKDFSalt returns salt for the pbkdf2 function used to encrypt JWT payload
92+
func GetPBKDFSalt() string {
93+
return env.Get(McsPBKDFSalt, defaultPBKDFSalt)
94+
}

pkg/auth/jwt/const.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2020 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package jwt
18+
19+
const (
20+
McsHmacJWTSecret = "MCS_HMAC_JWT_SECRET"
21+
McsSTSAndJWTDurationSeconds = "MCS_STS_AND_JWT_DURATION_SECONDS"
22+
McsPBKDFPassphrase = "MCS_PBKDF_PASSPHRASE"
23+
McsPBKDFSalt = "MCS_PBKDF_SALT"
24+
)

0 commit comments

Comments
 (0)