diff --git a/operatorapi/operator_login.go b/operatorapi/operator_login.go index 362bb84ec..81481b8a5 100644 --- a/operatorapi/operator_login.go +++ b/operatorapi/operator_login.go @@ -39,7 +39,7 @@ import ( func registerLoginHandlers(api *operations.OperatorAPI) { // GET login strategy api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder { - loginDetails, err := getLoginDetailsResponse() + loginDetails, err := getLoginDetailsResponse(params.HTTPRequest) if err != nil { return user_api.NewLoginDetailDefault(int(err.Code)).WithPayload(err) } @@ -60,7 +60,7 @@ func registerLoginHandlers(api *operations.OperatorAPI) { }) // POST login using external IDP api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder { - loginResponse, err := getLoginOauth2AuthResponse(params.Body) + loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body) if err != nil { return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err) } @@ -91,14 +91,14 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) { } // getLoginDetailsResponse returns information regarding the Console authentication mechanism. -func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) { +func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) { loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount redirectURL := "" if oauth2.IsIDPEnabled() { loginStrategy = models.LoginDetailsLoginStrategyRedirect // initialize new oauth2 client - oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, restapi.GetConsoleHTTPClient()) + oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient()) if err != nil { return nil, prepareError(err) } @@ -123,12 +123,12 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, return oauth2Token, nil } -func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { +func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if oauth2.IsIDPEnabled() { // initialize new oauth2 client - oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, restapi.GetConsoleHTTPClient()) + oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient()) if err != nil { return nil, prepareError(err) } diff --git a/pkg/auth/idp/oauth2/config.go b/pkg/auth/idp/oauth2/config.go index 832b2bae8..df09d6948 100644 --- a/pkg/auth/idp/oauth2/config.go +++ b/pkg/auth/idp/oauth2/config.go @@ -45,15 +45,19 @@ func GetIDPSecret() string { return env.Get(ConsoleIDPSecret, "") } -// Public endpoint used by the identity oidcProvider when redirecting the user after identity verification +// Public endpoint used by the identity oidcProvider when redirecting +// the user after identity verification func GetIDPCallbackURL() string { return env.Get(ConsoleIDPCallbackURL, "") } +func GetIDPCallbackURLDynamic() bool { + return env.Get(ConsoleIDPCallbackURLDynamic, "") == "on" +} + func IsIDPEnabled() bool { return GetIDPURL() != "" && - GetIDPClientID() != "" && - GetIDPCallbackURL() != "" + GetIDPClientID() != "" } var defaultPassphraseForIDPHmac = utils.RandomCharString(64) diff --git a/pkg/auth/idp/oauth2/const.go b/pkg/auth/idp/oauth2/const.go index 58f99df51..6fe1971a2 100644 --- a/pkg/auth/idp/oauth2/const.go +++ b/pkg/auth/idp/oauth2/const.go @@ -18,14 +18,15 @@ package oauth2 // Environment constants for console IDP/SSO configuration const ( - 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" - ConsoleIDPScopes = "CONSOLE_IDP_SCOPES" - ConsoleIDPUserInfo = "CONSOLE_IDP_USERINFO" - ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION" + ConsoleMinIOServer = "CONSOLE_MINIO_SERVER" + ConsoleIDPURL = "CONSOLE_IDP_URL" + ConsoleIDPClientID = "CONSOLE_IDP_CLIENT_ID" + ConsoleIDPSecret = "CONSOLE_IDP_SECRET" + ConsoleIDPCallbackURL = "CONSOLE_IDP_CALLBACK" + ConsoleIDPCallbackURLDynamic = "CONSOLE_IDP_CALLBACK_DYNAMIC" + 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 be7274d10..c69efd072 100644 --- a/pkg/auth/idp/oauth2/provider.go +++ b/pkg/auth/idp/oauth2/provider.go @@ -119,11 +119,33 @@ var derivedKey = func() []byte { return pbkdf2.Key([]byte(getPassphraseForIDPHmac()), []byte(getSaltForIDPHmac()), 4096, 32, sha1.New) } +const ( + schemeHTTP = "http" + schemeHTTPS = "https" +) + +func getLoginCallbackURL(r *http.Request) string { + scheme := getSourceScheme(r) + if scheme == "" { + if r.TLS != nil { + scheme = schemeHTTPS + } else { + scheme = schemeHTTP + } + } + + redirectURL := scheme + "://" + r.Host + "/oauth_callback" + _, err := url.Parse(redirectURL) + if err != nil { + panic(err) + } + return redirectURL +} + // 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(scopes []string, httpClient *http.Client) (*Provider, error) { - +func NewOauth2ProviderClient(scopes []string, r *http.Request, httpClient *http.Client) (*Provider, error) { ddoc, err := parseDiscoveryDoc(GetIDPURL(), httpClient) if err != nil { return nil, err @@ -134,6 +156,13 @@ func NewOauth2ProviderClient(scopes []string, httpClient *http.Client) (*Provide scopes = strings.Split(getIDPScopes(), ",") } + redirectURL := GetIDPCallbackURL() + if GetIDPCallbackURLDynamic() { + // dynamic redirect if set, will generate redirect URLs + // dynamically based on incoming requests. + redirectURL = getLoginCallbackURL(r) + } + // add "openid" scope always. scopes = append(scopes, "openid") @@ -141,7 +170,7 @@ func NewOauth2ProviderClient(scopes []string, httpClient *http.Client) (*Provide client.oauth2Config = &xoauth2.Config{ ClientID: GetIDPClientID(), ClientSecret: GetIDPSecret(), - RedirectURL: GetIDPCallbackURL(), + RedirectURL: redirectURL, Endpoint: oauth2.Endpoint{ AuthURL: ddoc.AuthEndpoint, TokenURL: ddoc.TokenEndpoint, diff --git a/pkg/auth/idp/oauth2/proxy.go b/pkg/auth/idp/oauth2/proxy.go new file mode 100644 index 000000000..c6d40afe6 --- /dev/null +++ b/pkg/auth/idp/oauth2/proxy.go @@ -0,0 +1,70 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2021 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 . + +package oauth2 + +import ( + "net/http" + "regexp" + "strings" +) + +var ( + // De-facto standard header keys. + xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") + xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme") +) + +var ( + // RFC7239 defines a new "Forwarded: " header designed to replace the + // existing use of X-Forwarded-* headers. + // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43 + forwarded = http.CanonicalHeaderKey("Forwarded") + // Allows for a sub-match of the first value after 'for=' to the next + // comma, semi-colon or space. The match is case-insensitive. + forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)(.*)`) + // Allows for a sub-match for the first instance of scheme (http|https) + // prefixed by 'proto='. The match is case-insensitive. + protoRegex = regexp.MustCompile(`(?i)^(;|,| )+(?:proto=)(https|http)`) +) + +// getSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239 +// Forwarded headers (in that order). +func getSourceScheme(r *http.Request) string { + var scheme string + + // Retrieve the scheme from X-Forwarded-Proto. + if proto := r.Header.Get(xForwardedProto); proto != "" { + scheme = strings.ToLower(proto) + } else if proto = r.Header.Get(xForwardedScheme); proto != "" { + scheme = strings.ToLower(proto) + } else if proto := r.Header.Get(forwarded); proto != "" { + // match should contain at least two elements if the protocol was + // specified in the Forwarded header. The first element will always be + // the 'for=', which we ignore, subsequently we proceed to look for + // 'proto=' which should precede right after `for=` if not + // we simply ignore the values and return empty. This is in line + // with the approach we took for returning first ip from multiple + // params. + if match := forRegex.FindStringSubmatch(proto); len(match) > 1 { + if match = protoRegex.FindStringSubmatch(match[2]); len(match) > 1 { + scheme = strings.ToLower(match[2]) + } + } + } + + return scheme +} diff --git a/restapi/user_login.go b/restapi/user_login.go index faa9f1849..5402b5653 100644 --- a/restapi/user_login.go +++ b/restapi/user_login.go @@ -38,7 +38,7 @@ import ( func registerLoginHandlers(api *operations.ConsoleAPI) { // GET login strategy api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder { - loginDetails, err := getLoginDetailsResponse() + loginDetails, err := getLoginDetailsResponse(params.HTTPRequest) if err != nil { return user_api.NewLoginDetailDefault(int(err.Code)).WithPayload(err) } @@ -59,7 +59,7 @@ func registerLoginHandlers(api *operations.ConsoleAPI) { }) // POST login using external IDP api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder { - loginResponse, err := getLoginOauth2AuthResponse(params.Body) + loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body) if err != nil { return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err) } @@ -131,14 +131,14 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.E } // getLoginDetailsResponse returns information regarding the Console authentication mechanism. -func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) { +func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) { loginStrategy := models.LoginDetailsLoginStrategyForm redirectURL := "" if oauth2.IsIDPEnabled() { loginStrategy = models.LoginDetailsLoginStrategyRedirect // initialize new oauth2 client - oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, GetConsoleHTTPClient()) + oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient()) if err != nil { return nil, prepareError(err, errOauth2Provider) } @@ -164,12 +164,12 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, return userCredentials, nil } -func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { +func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if oauth2.IsIDPEnabled() { // initialize new oauth2 client - oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, GetConsoleHTTPClient()) + oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient()) if err != nil { return nil, prepareError(err) }