From b6f818bdd3772ef23114c46991374e6eb637c52a Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Tue, 14 Sep 2021 16:16:29 -0700 Subject: [PATCH] fix: add support for userInfo endpoint (#1048) With support for GitLab OpenID service, we need to make sure to support userInfo endpoint for this MinIO server requires that access_token is additionally sent along with id_token as before to make sure we can fetch additional claims from /userinfo endpoint. This PR brings support on console for this feature. Refer https://github.com/minio/minio/pull/12469 --- go.mod | 6 ++-- go.sum | 11 +++---- operatorapi/operator_login.go | 2 +- pkg/auth/idp/oauth2/config.go | 52 ++++++++++++++++++--------------- pkg/auth/idp/oauth2/const.go | 15 +++++----- pkg/auth/idp/oauth2/provider.go | 50 +++++++++++++++++-------------- restapi/user_login.go | 4 +-- 7 files changed, 76 insertions(+), 64 deletions(-) diff --git a/go.mod b/go.mod index 8522f0604..4a63fd035 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,12 @@ require ( github.com/minio/cli v1.22.0 github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf github.com/minio/kes v0.11.0 - github.com/minio/madmin-go v1.0.17 + github.com/minio/madmin-go v1.1.5 github.com/minio/mc v0.0.0-20210626002108-cebf3318546f - github.com/minio/minio-go/v7 v7.0.13-0.20210715203016-9e713532886e + github.com/minio/minio-go/v7 v7.0.14 github.com/minio/operator v0.0.0-20210812082324-26350f153661 github.com/minio/operator/logsearchapi v0.0.0-20210812082324-26350f153661 - github.com/minio/pkg v1.0.8 + github.com/minio/pkg v1.1.3 github.com/minio/selfupdate v0.3.1 github.com/mitchellh/go-homedir v1.1.0 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect diff --git a/go.sum b/go.sum index 79af2c40d..8e59910e1 100644 --- a/go.sum +++ b/go.sum @@ -877,8 +877,8 @@ github.com/minio/filepath v1.0.0/go.mod h1:/nRZA2ldl5z6jT9/KQuvZcQlxZIMQoFFQPvEX github.com/minio/kes v0.11.0 h1:8ma6OCVSxKT50b1uYXLJro3m7PmZtCLxBaTddQexI5k= github.com/minio/kes v0.11.0/go.mod h1:mTF1Bv8YVEtQqF/B7Felp4tLee44Pp+dgI0rhCvgNg8= github.com/minio/madmin-go v1.0.12/go.mod h1:BK+z4XRx7Y1v8SFWXsuLNqQqnq5BO/axJ8IDJfgyvfs= -github.com/minio/madmin-go v1.0.17 h1:VMEn4nMKf0X3uNH0u+fZcn17KSwVkQGwyER/igG556E= -github.com/minio/madmin-go v1.0.17/go.mod h1:4nl9hvLWFnwCjkLfZSsZXEHgDODa2XSG6xGlIZyQ2oA= +github.com/minio/madmin-go v1.1.5 h1:xfzHwQ/KeKDQZKLqllNSyexwOPM/tvc13UdCeVMzADY= +github.com/minio/madmin-go v1.1.5/go.mod h1:xIPJHUbyYhNDgeD9Wov5Fz5/p7DIW0u+q6Rs/+Xu2TM= github.com/minio/mc v0.0.0-20210626002108-cebf3318546f h1:hyFvo5hSFw2K417YvDr/vAKlgCG69uTuhZW/5LNdL0U= github.com/minio/mc v0.0.0-20210626002108-cebf3318546f/go.mod h1:tuaonkPjVApCXkbtKENHBtsqUf7YTV33qmFrC+Pgp5g= github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= @@ -886,16 +886,17 @@ github.com/minio/md5-simd v1.1.1 h1:9ojcLbuZ4gXbB2sX53MKn8JUZ0sB/2wfwsEcRw+I08U= github.com/minio/md5-simd v1.1.1/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= github.com/minio/minio-go/v7 v7.0.11-0.20210302210017-6ae69c73ce78/go.mod h1:mTh2uJuAbEqdhMVl6CMIIZLUeiMiWtJR4JB8/5g2skw= github.com/minio/minio-go/v7 v7.0.11-0.20210607181445-e162fdb8e584/go.mod h1:WoyW+ySKAKjY98B9+7ZbI8z8S3jaxaisdcvj9TGlazA= -github.com/minio/minio-go/v7 v7.0.13-0.20210715203016-9e713532886e h1:aVnxKPpUI1gVeEf9vC+QEt8OxMXiiNMeUWcrBM62oDU= -github.com/minio/minio-go/v7 v7.0.13-0.20210715203016-9e713532886e/go.mod h1:S23iSP5/gbMwtxeY5FM71R+TkAYyzEdoNEDDwpt8yWs= +github.com/minio/minio-go/v7 v7.0.14 h1:T7cw8P586gVwEEd0y21kTYtloD576XZgP62N8pE130s= +github.com/minio/minio-go/v7 v7.0.14/go.mod h1:S23iSP5/gbMwtxeY5FM71R+TkAYyzEdoNEDDwpt8yWs= github.com/minio/operator v0.0.0-20210812082324-26350f153661 h1:dGAJHpfmhNukFg0M0wDqH+G1OB2YPgZCcT6uv4n9YQk= github.com/minio/operator v0.0.0-20210812082324-26350f153661/go.mod h1:zQqn6VGT46xlSpVXh1I/VZRv+eSgHtVu6URdg71YKX8= github.com/minio/operator/logsearchapi v0.0.0-20210812082324-26350f153661 h1:tJw15hS3b1dVTf5PwA4roXZ/oRNnHyZ/8Y+yNTmQ5rA= github.com/minio/operator/logsearchapi v0.0.0-20210812082324-26350f153661/go.mod h1:R+38Pf3wfm+JMiyLPb/r8OMrBm0vK2hZgUT4y4aYoSY= github.com/minio/pkg v1.0.3/go.mod h1:obU54TZ9QlMv0TRaDgQ/JTzf11ZSXxnSfLrm4tMtBP8= github.com/minio/pkg v1.0.4/go.mod h1:obU54TZ9QlMv0TRaDgQ/JTzf11ZSXxnSfLrm4tMtBP8= -github.com/minio/pkg v1.0.8 h1:lWQwHSeYlvnRoPpO+wS0I4mL6c00ABxBgbGjSmjwOi4= github.com/minio/pkg v1.0.8/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14= +github.com/minio/pkg v1.1.3 h1:J4vGnlNSxc/o9gDOQMZ3k0L3koA7ZgBQ7GRMrUpt/OY= +github.com/minio/pkg v1.1.3/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14= github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs= github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= diff --git a/operatorapi/operator_login.go b/operatorapi/operator_login.go index 892ee05f6..8c2c736f5 100644 --- a/operatorapi/operator_login.go +++ b/operatorapi/operator_login.go @@ -185,7 +185,7 @@ func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) { loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount redirectURL := "" - if oauth2.IsIdpEnabled() { + if oauth2.IsIDPEnabled() { loginStrategy = models.LoginDetailsLoginStrategyRedirect // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil, restapi.GetConsoleHTTPClient()) diff --git a/pkg/auth/idp/oauth2/config.go b/pkg/auth/idp/oauth2/config.go index 96a8b62f7..832b2bae8 100644 --- a/pkg/auth/idp/oauth2/config.go +++ b/pkg/auth/idp/oauth2/config.go @@ -29,49 +29,53 @@ func GetSTSEndpoint() string { return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000")) } -func GetIdpURL() string { - return env.Get(ConsoleIdpURL, "") +func GetIDPURL() string { + return env.Get(ConsoleIDPURL, "") } -func GetIdpClientID() string { - return env.Get(ConsoleIdpClientID, "") +func GetIDPClientID() string { + return env.Get(ConsoleIDPClientID, "") } -func GetIdpSecret() string { - return env.Get(ConsoleIdpSecret, "") +func GetIDPUserInfo() bool { + return env.Get(ConsoleIDPUserInfo, "") == "on" +} + +func GetIDPSecret() string { + return env.Get(ConsoleIDPSecret, "") } // Public endpoint used by the identity oidcProvider when redirecting the user after identity verification -func GetIdpCallbackURL() string { - return env.Get(ConsoleIdpCallbackURL, "") +func GetIDPCallbackURL() string { + return env.Get(ConsoleIDPCallbackURL, "") } -func IsIdpEnabled() bool { - return GetIdpURL() != "" && - GetIdpClientID() != "" && - GetIdpCallbackURL() != "" +func IsIDPEnabled() bool { + return GetIDPURL() != "" && + GetIDPClientID() != "" && + GetIDPCallbackURL() != "" } -var defaultPassphraseForIdpHmac = utils.RandomCharString(64) +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(ConsoleIdpHmacPassphrase, defaultPassphraseForIdpHmac) +// GetPassphraseForIDPHmac returns passphrase for the pbkdf2 function used to sign the oauth2 state parameter +func getPassphraseForIDPHmac() string { + return env.Get(ConsoleIDPHmacPassphrase, defaultPassphraseForIDPHmac) } -var defaultSaltForIdpHmac = utils.RandomCharString(64) +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(ConsoleIdpHmacSalt, defaultSaltForIdpHmac) +// GetSaltForIDPHmac returns salt for the pbkdf2 function used to sign the oauth2 state parameter +func getSaltForIDPHmac() string { + return env.Get(ConsoleIDPHmacSalt, defaultSaltForIDPHmac) } -// getIdpScopes return default scopes during the IDP login request -func getIdpScopes() string { +// getIDPScopes return default scopes during the IDP login request +func getIDPScopes() string { return env.Get(ConsoleIDPScopes, "openid,profile,email") } -// getIdpTokenExpiration return default token expiration for access token (in seconds) -func getIdpTokenExpiration() string { +// getIDPTokenExpiration return default token expiration for access token (in seconds) +func getIDPTokenExpiration() string { return env.Get(ConsoleIDPTokenExpiration, "3600") } diff --git a/pkg/auth/idp/oauth2/const.go b/pkg/auth/idp/oauth2/const.go index 3bfe3c1db..58f99df51 100644 --- a/pkg/auth/idp/oauth2/const.go +++ b/pkg/auth/idp/oauth2/const.go @@ -16,15 +16,16 @@ package oauth2 +// Environment constants for console IDP/SSO configuration const ( - // const for idp configuration ConsoleMinIOServer = "CONSOLE_MINIO_SERVER" - ConsoleIdpURL = "CONSOLE_IDP_URL" - ConsoleIdpClientID = "CONSOLE_IDP_CLIENT_ID" - ConsoleIdpSecret = "CONSOLE_IDP_SECRET" - ConsoleIdpCallbackURL = "CONSOLE_IDP_CALLBACK" - ConsoleIdpHmacPassphrase = "CONSOLE_IDP_HMAC_PASSPHRASE" - ConsoleIdpHmacSalt = "CONSOLE_IDP_HMAC_SALT" + ConsoleIDPURL = "CONSOLE_IDP_URL" + ConsoleIDPClientID = "CONSOLE_IDP_CLIENT_ID" + ConsoleIDPSecret = "CONSOLE_IDP_SECRET" + ConsoleIDPCallbackURL = "CONSOLE_IDP_CALLBACK" + ConsoleIDPHmacPassphrase = "CONSOLE_IDP_HMAC_PASSPHRASE" + ConsoleIDPHmacSalt = "CONSOLE_IDP_HMAC_SALT" ConsoleIDPScopes = "CONSOLE_IDP_SCOPES" + ConsoleIDPUserInfo = "CONSOLE_IDP_USERINFO" ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION" ) diff --git a/pkg/auth/idp/oauth2/provider.go b/pkg/auth/idp/oauth2/provider.go index 78f37c9e7..869403d39 100644 --- a/pkg/auth/idp/oauth2/provider.go +++ b/pkg/auth/idp/oauth2/provider.go @@ -88,7 +88,9 @@ type Provider struct { // often available via site-specific packages, such as // google.Endpoint or github.Endpoint. // - Scopes specifies optional requested permissions. - ClientID string + ClientID string + // if enabled means that we need extrace access_token as well + UserInfo bool oauth2Config Configuration oidcProvider *oidc.Provider provHTTPClient *http.Client @@ -97,7 +99,7 @@ type Provider struct { // derivedKey is the key used to compute the HMAC for signing the oauth state parameter // its derived using pbkdf on CONSOLE_IDP_HMAC_PASSPHRASE with CONSOLE_IDP_HMAC_SALT var derivedKey = func() []byte { - return pbkdf2.Key([]byte(getPassphraseForIdpHmac()), []byte(getSaltForIdpHmac()), 4096, 32, sha1.New) + return pbkdf2.Key([]byte(getPassphraseForIDPHmac()), []byte(getSaltForIDPHmac()), 4096, 32, sha1.New) } // NewOauth2ProviderClient instantiates a new oauth2 client using the configured credentials @@ -105,34 +107,30 @@ var derivedKey = func() []byte { // oauth2 authentication flow func NewOauth2ProviderClient(ctx context.Context, scopes []string, httpClient *http.Client) (*Provider, error) { customCtx := oidc.ClientContext(ctx, httpClient) - provider, err := oidc.NewProvider(customCtx, GetIdpURL()) + provider, err := oidc.NewProvider(customCtx, GetIDPURL()) if err != nil { return nil, err } - // if google, change scopes - u, err := url.Parse(GetIdpURL()) - if err != nil { - return nil, err - } - // below verification should not be necessary if the user configure exactly the - // scopes he need, will be removed on a future release - if u.Host == "google.com" { - scopes = []string{oidc.ScopeOpenID} - } + // If provided scopes are empty we use a default list or the user configured list if len(scopes) == 0 { - scopes = strings.Split(getIdpScopes(), ",") + scopes = strings.Split(getIDPScopes(), ",") } + + // add "openid" scope always. + scopes = append(scopes, oidc.ScopeOpenID) + client := new(Provider) client.oauth2Config = &xoauth2.Config{ - ClientID: GetIdpClientID(), - ClientSecret: GetIdpSecret(), - RedirectURL: GetIdpCallbackURL(), + ClientID: GetIDPClientID(), + ClientSecret: GetIDPSecret(), + RedirectURL: GetIDPCallbackURL(), Endpoint: provider.Endpoint(), Scopes: scopes, } client.oidcProvider = provider - client.ClientID = GetIdpClientID() + client.ClientID = GetIDPClientID() + client.UserInfo = GetIDPUserInfo() client.provHTTPClient = httpClient return client, nil @@ -184,18 +182,26 @@ func (client *Provider) VerifyIdentity(ctx context.Context, code, state string) // check if user configured a hardcoded expiration for console via env variables // and override the incoming expiration - userConfiguredExpiration := getIdpTokenExpiration() + userConfiguredExpiration := getIDPTokenExpiration() if userConfiguredExpiration != "" { expiration, _ = strconv.Atoi(userConfiguredExpiration) } idToken := oauth2Token.Extra("id_token") if idToken == nil { - return nil, errors.New("returned token is missing id_token claim") + return nil, errors.New("missing id_token") } - return &credentials.WebIdentityToken{ + token := &credentials.WebIdentityToken{ Token: idToken.(string), Expiry: expiration, - }, nil + } + if client.UserInfo { // look for access_token only if userinfo is requested. + accessToken := oauth2Token.Extra("access_token") + if accessToken == nil { + return nil, errors.New("missing access_token") + } + token.AccessToken = accessToken.(string) + } + return token, nil } stsEndpoint := GetSTSEndpoint() sts := credentials.New(&credentials.STSWebIdentity{ diff --git a/restapi/user_login.go b/restapi/user_login.go index 68138397d..eaf6cc125 100644 --- a/restapi/user_login.go +++ b/restapi/user_login.go @@ -183,7 +183,7 @@ func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) { loginStrategy := models.LoginDetailsLoginStrategyForm redirectURL := "" - if oauth2.IsIdpEnabled() { + if oauth2.IsIDPEnabled() { loginStrategy = models.LoginDetailsLoginStrategyRedirect // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil, GetConsoleHTTPClient()) @@ -215,7 +215,7 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - if oauth2.IsIdpEnabled() { + if oauth2.IsIDPEnabled() { // initialize new oauth2 client oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil, GetConsoleHTTPClient()) if err != nil {