idp integration for mcs (#75)
This PR adds support for oidc in mcs, to enable idp authentication you need to pass the following environment variables and restart mcs. ``` MCS_IDP_URL="" MCS_IDP_CLIENT_ID="" MCS_IDP_SECRET="" MCS_IDP_CALLBACK="" ```
This commit is contained in:
49
pkg/auth/idp.go
Normal file
49
pkg/auth/idp.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/minio/mcs/pkg/auth/idp/oauth2"
|
||||
)
|
||||
|
||||
// IdentityProviderClient interface with all functions to be implemented
|
||||
// by mock when testing, it should include all IdentityProviderClient respective api calls
|
||||
// that are used within this project.
|
||||
type IdentityProviderClient interface {
|
||||
VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error)
|
||||
GenerateLoginURL() string
|
||||
}
|
||||
|
||||
// Interface implementation
|
||||
//
|
||||
// Define the structure of a IdentityProvider Client and define the functions that are actually used
|
||||
// during the authentication flow.
|
||||
type IdentityProvider struct {
|
||||
Client IdentityProviderClient
|
||||
}
|
||||
|
||||
// VerifyIdentity will verify the user identity against the idp using the authorization code flow
|
||||
func (c IdentityProvider) VerifyIdentity(ctx context.Context, code, state string) (*oauth2.User, error) {
|
||||
return c.Client.VerifyIdentity(ctx, code, state)
|
||||
}
|
||||
|
||||
// GenerateLoginURL returns a new URL used by the user to login against the idp
|
||||
func (c IdentityProvider) GenerateLoginURL() string {
|
||||
return c.Client.GenerateLoginURL()
|
||||
}
|
||||
71
pkg/auth/idp/oauth2/config.go
Normal file
71
pkg/auth/idp/oauth2/config.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package oauth2 contains all the necessary configurations to initialize the
|
||||
// idp communication using oauth2 protocol
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"github.com/minio/mcs/pkg/auth/utils"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
func GetIdpURL() string {
|
||||
return env.Get(McsIdpURL, "")
|
||||
}
|
||||
|
||||
func GetIdpClientID() string {
|
||||
return env.Get(McsIdpClientID, "")
|
||||
}
|
||||
|
||||
func GetIdpSecret() string {
|
||||
return env.Get(McsIdpSecret, "")
|
||||
}
|
||||
|
||||
// Public endpoint used by the identity oidcProvider when redirecting the user after identity verification
|
||||
func GetIdpCallbackURL() string {
|
||||
return env.Get(McsIdpCallbackURL, "")
|
||||
}
|
||||
|
||||
func GetIdpAdminRoles() string {
|
||||
return env.Get(McsIdpAdminRoles, "")
|
||||
}
|
||||
|
||||
func IsIdpEnabled() bool {
|
||||
return GetIdpURL() != "" &&
|
||||
GetIdpClientID() != "" &&
|
||||
GetIdpSecret() != "" &&
|
||||
GetIdpCallbackURL() != ""
|
||||
}
|
||||
|
||||
var defaultPassphraseForIdpHmac = utils.RandomCharString(64)
|
||||
|
||||
// GetPassphraseForIdpHmac returns passphrase for the pbkdf2 function used to sign the oauth2 state parameter
|
||||
func getPassphraseForIdpHmac() string {
|
||||
return env.Get(McsIdpHmacPassphrase, defaultPassphraseForIdpHmac)
|
||||
}
|
||||
|
||||
var defaultSaltForIdpHmac = utils.RandomCharString(64)
|
||||
|
||||
// GetSaltForIdpHmac returns salt for the pbkdf2 function used to sign the oauth2 state parameter
|
||||
func getSaltForIdpHmac() string {
|
||||
return env.Get(McsIdpHmacSalt, defaultSaltForIdpHmac)
|
||||
}
|
||||
|
||||
// GetSaltForIdpHmac returns the policy to be assigned to the users authenticating via an IDP
|
||||
func GetIDPPolicyForUser() string {
|
||||
return env.Get(McsIdpPolicyUser, "mcsAdmin")
|
||||
}
|
||||
29
pkg/auth/idp/oauth2/const.go
Normal file
29
pkg/auth/idp/oauth2/const.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package oauth2
|
||||
|
||||
const (
|
||||
// const for idp configuration
|
||||
McsIdpURL = "MCS_IDP_URL"
|
||||
McsIdpClientID = "MCS_IDP_CLIENT_ID"
|
||||
McsIdpSecret = "MCS_IDP_SECRET"
|
||||
McsIdpCallbackURL = "MCS_IDP_CALLBACK"
|
||||
McsIdpAdminRoles = "MCS_IDP_ADMIN_ROLES"
|
||||
McsIdpHmacPassphrase = "MCS_IDP_HMAC_PASSPHRASE"
|
||||
McsIdpHmacSalt = "MCS_IDP_HMAC_SALT"
|
||||
McsIdpPolicyUser = "MCS_IDP_POLICY_USER"
|
||||
)
|
||||
229
pkg/auth/idp/oauth2/provider.go
Normal file
229
pkg/auth/idp/oauth2/provider.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/minio/mcs/pkg/auth/utils"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
xoauth2 "golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var (
|
||||
errGeneric = errors.New("an error occurred, please try again")
|
||||
)
|
||||
|
||||
type Configuration interface {
|
||||
Exchange(ctx context.Context, code string, opts ...xoauth2.AuthCodeOption) (*xoauth2.Token, error)
|
||||
AuthCodeURL(state string, opts ...xoauth2.AuthCodeOption) string
|
||||
PasswordCredentialsToken(ctx context.Context, username string, password string) (*xoauth2.Token, error)
|
||||
Client(ctx context.Context, t *xoauth2.Token) *http.Client
|
||||
TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
xoauth2.Config
|
||||
}
|
||||
|
||||
func (ac Config) Exchange(ctx context.Context, code string, opts ...xoauth2.AuthCodeOption) (*xoauth2.Token, error) {
|
||||
return ac.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (ac Config) AuthCodeURL(state string, opts ...xoauth2.AuthCodeOption) string {
|
||||
return ac.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (ac Config) PasswordCredentialsToken(ctx context.Context, username string, password string) (*xoauth2.Token, error) {
|
||||
return ac.PasswordCredentialsToken(ctx, username, password)
|
||||
}
|
||||
|
||||
func (ac Config) Client(ctx context.Context, t *xoauth2.Token) *http.Client {
|
||||
return ac.Client(ctx, t)
|
||||
}
|
||||
|
||||
func (ac Config) TokenSource(ctx context.Context, t *xoauth2.Token) xoauth2.TokenSource {
|
||||
return ac.TokenSource(ctx, t)
|
||||
}
|
||||
|
||||
// Provider is a wrapper of the oauth2 configuration and the oidc provider
|
||||
type Provider struct {
|
||||
// oauth2Config is an interface configuration that contains the following fields
|
||||
// Config{
|
||||
// ClientID string
|
||||
// ClientSecret string
|
||||
// RedirectURL string
|
||||
// Endpoint oauth2.Endpoint
|
||||
// Scopes []string
|
||||
// }
|
||||
// - ClientID is the public identifier for this application
|
||||
// - ClientSecret is a shared secret between this application and the authorization server
|
||||
// - RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
// - Endpoint contains the resource server's token endpoint
|
||||
// URLs. These are constants specific to each server and are
|
||||
// often available via site-specific packages, such as
|
||||
// google.Endpoint or github.Endpoint.
|
||||
// - Scopes specifies optional requested permissions.
|
||||
ClientID string
|
||||
oauth2Config Configuration
|
||||
oidcProvider *oidc.Provider
|
||||
}
|
||||
|
||||
// derivedKey is the key used to compute the HMAC for signing the oauth state parameter
|
||||
// its derived using pbkdf on MCS_IDP_HMAC_PASSPHRASE with MCS_IDP_HMAC_SALT
|
||||
var derivedKey = pbkdf2.Key([]byte(getPassphraseForIdpHmac()), []byte(getSaltForIdpHmac()), 4096, 32, sha1.New)
|
||||
|
||||
// NewOauth2ProviderClient instantiates a new oauth2 client using the configured credentials
|
||||
// it returns a *Provider object that contains the necessary configuration to initiate an
|
||||
// oauth2 authentication flow
|
||||
func NewOauth2ProviderClient(ctx context.Context, scopes []string) (*Provider, error) {
|
||||
provider, err := oidc.NewProvider(ctx, GetIdpURL())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If provided scopes are empty we use a default list
|
||||
if len(scopes) == 0 {
|
||||
scopes = []string{oidc.ScopeOpenID, "profile", "app_metadata", "user_metadata", "email"}
|
||||
}
|
||||
client := new(Provider)
|
||||
config := xoauth2.Config{
|
||||
ClientID: GetIdpClientID(),
|
||||
ClientSecret: GetIdpSecret(),
|
||||
RedirectURL: GetIdpCallbackURL(),
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
}
|
||||
client.oauth2Config = &config
|
||||
client.oidcProvider = provider
|
||||
client.ClientID = GetIdpClientID()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
AppMetadata map[string]interface{} `json:"app_metadata"`
|
||||
Blocked bool `json:"blocked"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
FamilyName string `json:"family_name"`
|
||||
GivenName string `json:"given_name"`
|
||||
Identities []interface{} `json:"identities"`
|
||||
LastIP string `json:"last_ip"`
|
||||
LastLogin string `json:"last_login"`
|
||||
LastPasswordReset string `json:"last_password_reset"`
|
||||
LoginsCount int `json:"logins_count"`
|
||||
Mltifactor string `json:"multifactor"`
|
||||
Name string `json:"name"`
|
||||
Nickname string `json:"nickname"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
PhoneVerified bool `json:"phone_verified"`
|
||||
Picture string `json:"picture"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
UserID string `json:"user_id"`
|
||||
UserMetadata map[string]interface{} `json:"user_metadata"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// VerifyIdentity will contact the configured IDP and validate the user identity based on the authorization code
|
||||
func (client *Provider) VerifyIdentity(ctx context.Context, code, state string) (*User, error) {
|
||||
// verify the provided state is valid (prevents CSRF attacks)
|
||||
if !validateOauth2State(state) {
|
||||
return nil, errGeneric
|
||||
}
|
||||
// verify the authorization code against the identity oidcProvider
|
||||
// idp will return a token in exchange
|
||||
token, err := client.oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
log.Println("Failed to verify authorization code", err)
|
||||
return nil, errGeneric
|
||||
}
|
||||
// extract and check id_token field is provided in the response
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
log.Println("No id_token field in oauth2 token")
|
||||
return nil, errGeneric
|
||||
}
|
||||
config := &oidc.Config{
|
||||
ClientID: client.ClientID,
|
||||
}
|
||||
idToken, err := client.oidcProvider.Verifier(config).Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
log.Println("Failed to verify ID token", err)
|
||||
return nil, errGeneric
|
||||
}
|
||||
var profile User
|
||||
// Populate the profile object using the claims included in the token
|
||||
if err := idToken.Claims(&profile); err != nil {
|
||||
log.Println("Failed to read profile information", err)
|
||||
return nil, errGeneric
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
// validateOauth2State validates the provided state was originated using the same
|
||||
// instance (or one configured using the same secrets) of MCS, this is basically used to prevent CSRF attacks
|
||||
// https://security.stackexchange.com/questions/20187/oauth2-cross-site-request-forgery-and-state-parameter
|
||||
func validateOauth2State(state string) bool {
|
||||
// state contains a base64 encoded string that may ends with "==", the browser encodes that to "%3D%3D"
|
||||
// query unescape is need it before trying to decode the base64 string
|
||||
encodedMessage, err := url.QueryUnescape(state)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
// decode the state parameter value
|
||||
message, err := base64.StdEncoding.DecodeString(encodedMessage)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
s := strings.Split(string(message), ":")
|
||||
// Validate that the decoded message has the right format "message:hmac"
|
||||
if len(s) != 2 {
|
||||
return false
|
||||
}
|
||||
// extract the state and hmac
|
||||
incomingState, incomingHmac := s[0], s[1]
|
||||
// validate that hmac(incomingState + pbkdf2(secret, salt)) == incomingHmac
|
||||
return utils.ComputeHmac256(incomingState, derivedKey) == incomingHmac
|
||||
}
|
||||
|
||||
// GetRandomStateWithHMAC computes message + hmac(message, pbkdf2(key, salt)) to be used as state during the oauth authorization
|
||||
func GetRandomStateWithHMAC(length int) string {
|
||||
state := utils.RandomCharString(length)
|
||||
hmac := utils.ComputeHmac256(state, derivedKey)
|
||||
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", state, hmac)))
|
||||
}
|
||||
|
||||
// GenerateLoginURL returns a new login URL based on the configured IDP
|
||||
func (client *Provider) GenerateLoginURL() string {
|
||||
// generates random state and sign it using HMAC256
|
||||
state := GetRandomStateWithHMAC(25)
|
||||
loginURL := client.oauth2Config.AuthCodeURL(state)
|
||||
return strings.TrimSpace(loginURL)
|
||||
}
|
||||
98
pkg/auth/idp/oauth2/provider_test.go
Normal file
98
pkg/auth/idp/oauth2/provider_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Oauth2configMock struct{}
|
||||
|
||||
var oauth2ConfigExchangeMock func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||
var oauth2ConfigAuthCodeURLMock func(state string, opts ...oauth2.AuthCodeOption) string
|
||||
var oauth2ConfigPasswordCredentialsTokenMock func(ctx context.Context, username string, password string) (*oauth2.Token, error)
|
||||
var oauth2ConfigClientMock func(ctx context.Context, t *oauth2.Token) *http.Client
|
||||
var oauth2ConfigokenSourceMock func(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
|
||||
|
||||
func (ac Oauth2configMock) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return oauth2ConfigExchangeMock(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (ac Oauth2configMock) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return oauth2ConfigAuthCodeURLMock(state, opts...)
|
||||
}
|
||||
|
||||
func (ac Oauth2configMock) PasswordCredentialsToken(ctx context.Context, username string, password string) (*oauth2.Token, error) {
|
||||
return oauth2ConfigPasswordCredentialsTokenMock(ctx, username, password)
|
||||
}
|
||||
|
||||
func (ac Oauth2configMock) Client(ctx context.Context, t *oauth2.Token) *http.Client {
|
||||
return oauth2ConfigClientMock(ctx, t)
|
||||
}
|
||||
|
||||
func (ac Oauth2configMock) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource {
|
||||
return oauth2ConfigokenSourceMock(ctx, t)
|
||||
}
|
||||
|
||||
func TestGenerateLoginURL(t *testing.T) {
|
||||
funcAssert := assert.New(t)
|
||||
oauth2Provider := Provider{
|
||||
oauth2Config: Oauth2configMock{},
|
||||
oidcProvider: &oidc.Provider{},
|
||||
}
|
||||
// Test-1 : GenerateLoginURL() generates URL correctly with provided state
|
||||
oauth2ConfigAuthCodeURLMock = func(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
// Internally we are testing the private method getRandomStateWithHMAC, this function should always returns
|
||||
// a non-empty string
|
||||
return state
|
||||
}
|
||||
url := oauth2Provider.GenerateLoginURL()
|
||||
funcAssert.NotEqual("", url)
|
||||
}
|
||||
|
||||
func TestVerifyIdentity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
funcAssert := assert.New(t)
|
||||
// mock data
|
||||
oauth2Provider := Provider{
|
||||
oauth2Config: Oauth2configMock{},
|
||||
oidcProvider: &oidc.Provider{},
|
||||
}
|
||||
// Test-1 : VerifyIdentity() should fail because of bad state token
|
||||
_, err := oauth2Provider.VerifyIdentity(ctx, "AAABBBCCCDDDEEEFFF", "badtoken")
|
||||
funcAssert.NotNil(err)
|
||||
// Test-2 : VerifyIdentity() should fail because no id_token is provided by the idp
|
||||
oauth2ConfigExchangeMock = func(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return &oauth2.Token{}, nil
|
||||
}
|
||||
state := GetRandomStateWithHMAC(32)
|
||||
code := "AAABBBCCCDDDEEEFFF"
|
||||
_, err = oauth2Provider.VerifyIdentity(ctx, code, state)
|
||||
funcAssert.NotNil(err)
|
||||
// Test-3 : VerifyIdentity() should fail because no id_token is provided by the idp
|
||||
// TODO
|
||||
// Test-4 : VerifyIdentity() should fail because oidcProvider.Verifier returned an error
|
||||
// TODO
|
||||
// Test-5 : VerifyIdentity() should fail because idToken.Claims contains invalid fields
|
||||
// TODO
|
||||
}
|
||||
@@ -17,45 +17,15 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/mcs/pkg/auth/utils"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
// Do not use:
|
||||
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
|
||||
// for access/secret keys.
|
||||
|
||||
// The alphabet of random character string. Each character must be unique.
|
||||
//
|
||||
// The RandomCharString implementation requires that: 256 / len(letters) is a natural numbers.
|
||||
// For example: 256 / 64 = 4. However, 5 > 256/62 > 4 and therefore we must not use a alphabet
|
||||
// of 62 characters.
|
||||
// The reason is that if 256 / len(letters) is not a natural number then certain characters become
|
||||
// more likely then others.
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
|
||||
|
||||
func RandomCharString(n int) string {
|
||||
random := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, random); err != nil {
|
||||
panic(err) // Can only happen if we would run out of entropy.
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
for _, v := range random {
|
||||
j := v % byte(len(letters))
|
||||
s.WriteByte(letters[j])
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// defaultHmacJWTPassphrase will be used by default if application is not configured with a custom MCS_HMAC_JWT_SECRET secret
|
||||
var defaultHmacJWTPassphrase = RandomCharString(64)
|
||||
var defaultHmacJWTPassphrase = utils.RandomCharString(64)
|
||||
|
||||
// GetHmacJWTSecret returns the 64 bytes secret used for signing the generated JWT for the application
|
||||
func GetHmacJWTSecret() string {
|
||||
@@ -78,15 +48,14 @@ func GetMcsSTSAndJWTDurationTime() time.Duration {
|
||||
return time.Duration(duration) * time.Second
|
||||
}
|
||||
|
||||
// defaultPBKDFPassphrase
|
||||
var defaultPBKDFPassphrase = RandomCharString(64)
|
||||
var defaultPBKDFPassphrase = utils.RandomCharString(64)
|
||||
|
||||
// GetPBKDFPassphrase returns passphrase for the pbkdf2 function used to encrypt JWT payload
|
||||
func GetPBKDFPassphrase() string {
|
||||
return env.Get(McsPBKDFPassphrase, defaultPBKDFPassphrase)
|
||||
}
|
||||
|
||||
var defaultPBKDFSalt = RandomCharString(64)
|
||||
var defaultPBKDFSalt = utils.RandomCharString(64)
|
||||
|
||||
// GetPBKDFSalt returns salt for the pbkdf2 function used to encrypt JWT payload
|
||||
func GetPBKDFSalt() string {
|
||||
|
||||
60
pkg/auth/utils/utils.go
Normal file
60
pkg/auth/utils/utils.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Do not use:
|
||||
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
|
||||
// for access/secret keys.
|
||||
|
||||
// The alphabet of random character string. Each character must be unique.
|
||||
//
|
||||
// The RandomCharString implementation requires that: 256 / len(letters) is a natural numbers.
|
||||
// For example: 256 / 64 = 4. However, 5 > 256/62 > 4 and therefore we must not use a alphabet
|
||||
// of 62 characters.
|
||||
// The reason is that if 256 / len(letters) is not a natural number then certain characters become
|
||||
// more likely then others.
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
|
||||
|
||||
func RandomCharString(n int) string {
|
||||
random := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, random); err != nil {
|
||||
panic(err) // Can only happen if we would run out of entropy.
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
for _, v := range random {
|
||||
j := v % byte(len(letters))
|
||||
s.WriteByte(letters[j])
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func ComputeHmac256(message string, key []byte) string {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write([]byte(message))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
46
pkg/auth/utils/utils_test.go
Normal file
46
pkg/auth/utils/utils_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
func TestRandomCharString(t *testing.T) {
|
||||
funcAssert := assert.New(t)
|
||||
// Test-1 : RandomCharString() should return string with expected length
|
||||
length := 32
|
||||
token := RandomCharString(length)
|
||||
funcAssert.Equal(length, len(token))
|
||||
// Test-2 : RandomCharString() should output random string, new generated string should not be equal to the previous one
|
||||
newToken := RandomCharString(length)
|
||||
funcAssert.NotEqual(token, newToken)
|
||||
}
|
||||
|
||||
func TestComputeHmac256(t *testing.T) {
|
||||
funcAssert := assert.New(t)
|
||||
// Test-1 : ComputeHmac256() should return the right Hmac256 string based on a derived key
|
||||
var derivedKey = pbkdf2.Key([]byte("secret"), []byte("salt"), 4096, 32, sha1.New)
|
||||
var message = "hello world"
|
||||
var expectedHmac = "5r32q7W+0hcBnqzQwJJUDzVGoVivXGSodTcHSqG/9Q8="
|
||||
hmac := ComputeHmac256(message, derivedKey)
|
||||
funcAssert.Equal(hmac, expectedHmac)
|
||||
}
|
||||
Reference in New Issue
Block a user