Skip to content

Commit fc95ab8

Browse files
authored
Fix oauth state generation for OIDC login (#2333)
This is a regression from 118cf97 when env var support for passing console configuration from MinIO was removed. This change ensures that all MinIO nodes in a cluster are able to verify state tokens generated by other nodes in the cluster. Without this, it is necessary to use sticky sessions in a loadbalancer to ensure that OIDC authorization code login flow steps for a client happens on the same minio node. Fixes minio/minio#15527
1 parent bebe860 commit fc95ab8

File tree

6 files changed

+48
-21
lines changed

6 files changed

+48
-21
lines changed

operatorapi/login.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDet
111111
return nil, restapi.ErrorWithContext(ctx, err)
112112
}
113113
// Validate user against IDP
114-
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
114+
identityProvider := &auth.IdentityProvider{
115+
KeyFunc: oauth2.DefaultDerivedKey,
116+
Client: oauth2Client,
117+
}
115118
redirectURL = append(redirectURL, identityProvider.GenerateLoginURL())
116119
}
117120

@@ -146,7 +149,10 @@ func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.L
146149
return nil, restapi.ErrorWithContext(ctx, err)
147150
}
148151
// initialize new identity provider
149-
identityProvider := auth.IdentityProvider{Client: oauth2Client}
152+
identityProvider := auth.IdentityProvider{
153+
KeyFunc: oauth2.DefaultDerivedKey,
154+
Client: oauth2Client,
155+
}
150156
// Validate user against IDP
151157
_, err = verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State)
152158
if err != nil {

pkg/auth/idp.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,21 @@ type IdentityProviderI interface {
3838
// Define the structure of a IdentityProvider with Client inside and define the functions that are used
3939
// during the authentication flow.
4040
type IdentityProvider struct {
41-
Client *oauth2.Provider
41+
KeyFunc oauth2.StateKeyFunc
42+
Client *oauth2.Provider
4243
}
4344

4445
// VerifyIdentity will verify the user identity against the idp using the authorization code flow
4546
func (c IdentityProvider) VerifyIdentity(ctx context.Context, code, state string) (*credentials.Credentials, error) {
46-
return c.Client.VerifyIdentity(ctx, code, state)
47+
return c.Client.VerifyIdentity(ctx, code, state, c.KeyFunc)
4748
}
4849

4950
// VerifyIdentityForOperator will verify the user identity against the idp using the authorization code flow
5051
func (c IdentityProvider) VerifyIdentityForOperator(ctx context.Context, code, state string) (*xoauth2.Token, error) {
51-
return c.Client.VerifyIdentityForOperator(ctx, code, state)
52+
return c.Client.VerifyIdentityForOperator(ctx, code, state, c.KeyFunc)
5253
}
5354

5455
// GenerateLoginURL returns a new URL used by the user to login against the idp
5556
func (c IdentityProvider) GenerateLoginURL() string {
56-
return c.Client.GenerateLoginURL()
57+
return c.Client.GenerateLoginURL(c.KeyFunc)
5758
}

pkg/auth/idp/oauth2/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
package oauth2
2020

2121
import (
22+
"crypto/sha1"
2223
"strings"
2324

2425
"github.com/minio/console/pkg/auth/utils"
2526
"github.com/minio/pkg/env"
27+
"golang.org/x/crypto/pbkdf2"
2628
)
2729

2830
// ProviderConfig - OpenID IDP Configuration for console.
@@ -37,6 +39,14 @@ type ProviderConfig struct {
3739
RedirectCallback string
3840
}
3941

42+
// GetStateKeyFunc - return the key function used to generate the authorization
43+
// code flow state parameter.
44+
func (pc ProviderConfig) GetStateKeyFunc() StateKeyFunc {
45+
return func() []byte {
46+
return pbkdf2.Key([]byte(pc.HMACPassphrase), []byte(pc.HMACSalt), 4096, 32, sha1.New)
47+
}
48+
}
49+
4050
type OpenIDPCfg map[string]ProviderConfig
4151

4252
var DefaultIDPConfig = "_"

pkg/auth/idp/oauth2/provider.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ type Provider struct {
114114
provHTTPClient *http.Client
115115
}
116116

117-
// derivedKey is the key used to compute the HMAC for signing the oauth state parameter
117+
// DefaultDerivedKey is the key used to compute the HMAC for signing the oauth state parameter
118118
// its derived using pbkdf on CONSOLE_IDP_HMAC_PASSPHRASE with CONSOLE_IDP_HMAC_SALT
119-
var derivedKey = func() []byte {
119+
var DefaultDerivedKey = func() []byte {
120120
return pbkdf2.Key([]byte(getPassphraseForIDPHmac()), []byte(getSaltForIDPHmac()), 4096, 32, sha1.New)
121121
}
122122

@@ -304,11 +304,15 @@ type User struct {
304304
Username string `json:"username"`
305305
}
306306

307+
// StateKeyFunc - is a function that returns a key used in OAuth Authorization
308+
// flow state generation and verification.
309+
type StateKeyFunc func() []byte
310+
307311
// VerifyIdentity will contact the configured IDP to the user identity based on the authorization code and state
308312
// if the user is valid, then it will contact MinIO to get valid sts credentials based on the identity provided by the IDP
309-
func (client *Provider) VerifyIdentity(ctx context.Context, code, state string) (*credentials.Credentials, error) {
313+
func (client *Provider) VerifyIdentity(ctx context.Context, code, state string, keyFunc StateKeyFunc) (*credentials.Credentials, error) {
310314
// verify the provided state is valid (prevents CSRF attacks)
311-
if err := validateOauth2State(state); err != nil {
315+
if err := validateOauth2State(state, keyFunc); err != nil {
312316
return nil, err
313317
}
314318
getWebTokenExpiry := func() (*credentials.WebIdentityToken, error) {
@@ -357,9 +361,9 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state string)
357361
}
358362

359363
// VerifyIdentityForOperator will contact the configured IDP and validate the user identity based on the authorization code and state
360-
func (client *Provider) VerifyIdentityForOperator(ctx context.Context, code, state string) (*xoauth2.Token, error) {
364+
func (client *Provider) VerifyIdentityForOperator(ctx context.Context, code, state string, keyFunc StateKeyFunc) (*xoauth2.Token, error) {
361365
// verify the provided state is valid (prevents CSRF attacks)
362-
if err := validateOauth2State(state); err != nil {
366+
if err := validateOauth2State(state, keyFunc); err != nil {
363367
return nil, err
364368
}
365369
customCtx := context.WithValue(ctx, oauth2.HTTPClient, client.provHTTPClient)
@@ -376,7 +380,7 @@ func (client *Provider) VerifyIdentityForOperator(ctx context.Context, code, sta
376380
// validateOauth2State validates the provided state was originated using the same
377381
// instance (or one configured using the same secrets) of Console, this is basically used to prevent CSRF attacks
378382
// https://security.stackexchange.com/questions/20187/oauth2-cross-site-request-forgery-and-state-parameter
379-
func validateOauth2State(state string) error {
383+
func validateOauth2State(state string, keyFunc StateKeyFunc) error {
380384
// state contains a base64 encoded string that may ends with "==", the browser encodes that to "%3D%3D"
381385
// query unescape is need it before trying to decode the base64 string
382386
encodedMessage, err := url.QueryUnescape(state)
@@ -396,7 +400,7 @@ func validateOauth2State(state string) error {
396400
// extract the state and hmac
397401
incomingState, incomingHmac := s[0], s[1]
398402
// validate that hmac(incomingState + pbkdf2(secret, salt)) == incomingHmac
399-
if calculatedHmac := utils.ComputeHmac256(incomingState, derivedKey()); calculatedHmac != incomingHmac {
403+
if calculatedHmac := utils.ComputeHmac256(incomingState, keyFunc()); calculatedHmac != incomingHmac {
400404
return fmt.Errorf("oauth2 state is invalid, expected %s, got %s", calculatedHmac, incomingHmac)
401405
}
402406
return nil
@@ -429,16 +433,16 @@ func parseDiscoveryDoc(ustr string, httpClient *http.Client) (DiscoveryDoc, erro
429433
}
430434

431435
// GetRandomStateWithHMAC computes message + hmac(message, pbkdf2(key, salt)) to be used as state during the oauth authorization
432-
func GetRandomStateWithHMAC(length int) string {
436+
func GetRandomStateWithHMAC(length int, keyFunc StateKeyFunc) string {
433437
state := utils.RandomCharString(length)
434-
hmac := utils.ComputeHmac256(state, derivedKey())
438+
hmac := utils.ComputeHmac256(state, keyFunc())
435439
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", state, hmac)))
436440
}
437441

438442
// GenerateLoginURL returns a new login URL based on the configured IDP
439-
func (client *Provider) GenerateLoginURL() string {
443+
func (client *Provider) GenerateLoginURL(keyFunc StateKeyFunc) string {
440444
// generates random state and sign it using HMAC256
441-
state := GetRandomStateWithHMAC(25)
445+
state := GetRandomStateWithHMAC(25, keyFunc)
442446
loginURL := client.oauth2Config.AuthCodeURL(state)
443447
return strings.TrimSpace(loginURL)
444448
}

pkg/auth/idp/oauth2/provider_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ func TestGenerateLoginURL(t *testing.T) {
6666
// a non-empty string
6767
return state
6868
}
69-
url := oauth2Provider.GenerateLoginURL()
69+
url := oauth2Provider.GenerateLoginURL(DefaultDerivedKey)
7070
funcAssert.NotEqual("", url)
7171
}

restapi/user_login.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders o
162162
return nil, ErrorWithContext(ctx, err, ErrOauth2Provider)
163163
}
164164
// Validate user against IDP
165-
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
165+
identityProvider := &auth.IdentityProvider{
166+
KeyFunc: provider.GetStateKeyFunc(),
167+
Client: oauth2Client,
168+
}
166169
redirectURL = append(redirectURL, identityProvider.GenerateLoginURL())
167170
if provider.DisplayName != "" {
168171
displayNames = append(displayNames, provider.DisplayName)
@@ -201,7 +204,10 @@ func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProv
201204
return nil, ErrorWithContext(ctx, err)
202205
}
203206
// initialize new identity provider
204-
identityProvider := auth.IdentityProvider{Client: oauth2Client}
207+
identityProvider := auth.IdentityProvider{
208+
KeyFunc: openIDProviders[idpName].GetStateKeyFunc(),
209+
Client: oauth2Client,
210+
}
205211
// Validate user against IDP
206212
userCredentials, err := verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, *lr.State)
207213
if err != nil {

0 commit comments

Comments
 (0)