feat: Support dynamic redirect_uris based on incoming requests (#1227)
To enable this feature you need `CONSOLE_IDP_CALLBACK_DYNAMIC=on` ``` export CONSOLE_IDP_URL=https://gitlab.com/.well-known/openid-configuration export CONSOLE_IDP_CLIENT_ID="b0088c3836bb029393942f71ed7c8ac0add7f0856e6c86e67b0ff98f85c48658" export CONSOLE_IDP_SECRET="ed72087b37624e89816ac27c1355420902045274edd7baad2ae29b1b0e8436fe" export CONSOLE_IDP_SCOPES="openid,profile,email" export CONSOLE_IDP_USERINFO="on" export CONSOLE_IDP_CALLBACK_DYNAMIC=on console srv ``` if this becomes a common practice, we should enable this as default in future.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
70
pkg/auth/idp/oauth2/proxy.go
Normal file
70
pkg/auth/idp/oauth2/proxy.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user