Skip to content

Commit 373bfbf

Browse files
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.
1 parent b8417fb commit 373bfbf

File tree

6 files changed

+132
-28
lines changed

6 files changed

+132
-28
lines changed

operatorapi/operator_login.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import (
3939
func registerLoginHandlers(api *operations.OperatorAPI) {
4040
// GET login strategy
4141
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
42-
loginDetails, err := getLoginDetailsResponse()
42+
loginDetails, err := getLoginDetailsResponse(params.HTTPRequest)
4343
if err != nil {
4444
return user_api.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
4545
}
@@ -60,7 +60,7 @@ func registerLoginHandlers(api *operations.OperatorAPI) {
6060
})
6161
// POST login using external IDP
6262
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
63-
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
63+
loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body)
6464
if err != nil {
6565
return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
6666
}
@@ -91,14 +91,14 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) {
9191
}
9292

9393
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
94-
func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) {
94+
func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) {
9595
loginStrategy := models.LoginDetailsLoginStrategyServiceDashAccount
9696
redirectURL := ""
9797

9898
if oauth2.IsIDPEnabled() {
9999
loginStrategy = models.LoginDetailsLoginStrategyRedirect
100100
// initialize new oauth2 client
101-
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, restapi.GetConsoleHTTPClient())
101+
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient())
102102
if err != nil {
103103
return nil, prepareError(err)
104104
}
@@ -123,12 +123,12 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
123123
return oauth2Token, nil
124124
}
125125

126-
func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
126+
func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
127127
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
128128
defer cancel()
129129
if oauth2.IsIDPEnabled() {
130130
// initialize new oauth2 client
131-
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, restapi.GetConsoleHTTPClient())
131+
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, restapi.GetConsoleHTTPClient())
132132
if err != nil {
133133
return nil, prepareError(err)
134134
}

pkg/auth/idp/oauth2/config.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,19 @@ func GetIDPSecret() string {
4545
return env.Get(ConsoleIDPSecret, "")
4646
}
4747

48-
// Public endpoint used by the identity oidcProvider when redirecting the user after identity verification
48+
// Public endpoint used by the identity oidcProvider when redirecting
49+
// the user after identity verification
4950
func GetIDPCallbackURL() string {
5051
return env.Get(ConsoleIDPCallbackURL, "")
5152
}
5253

54+
func GetIDPCallbackURLDynamic() bool {
55+
return env.Get(ConsoleIDPCallbackURLDynamic, "") == "on"
56+
}
57+
5358
func IsIDPEnabled() bool {
5459
return GetIDPURL() != "" &&
55-
GetIDPClientID() != "" &&
56-
GetIDPCallbackURL() != ""
60+
GetIDPClientID() != ""
5761
}
5862

5963
var defaultPassphraseForIDPHmac = utils.RandomCharString(64)

pkg/auth/idp/oauth2/const.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ package oauth2
1818

1919
// Environment constants for console IDP/SSO configuration
2020
const (
21-
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
22-
ConsoleIDPURL = "CONSOLE_IDP_URL"
23-
ConsoleIDPClientID = "CONSOLE_IDP_CLIENT_ID"
24-
ConsoleIDPSecret = "CONSOLE_IDP_SECRET"
25-
ConsoleIDPCallbackURL = "CONSOLE_IDP_CALLBACK"
26-
ConsoleIDPHmacPassphrase = "CONSOLE_IDP_HMAC_PASSPHRASE"
27-
ConsoleIDPHmacSalt = "CONSOLE_IDP_HMAC_SALT"
28-
ConsoleIDPScopes = "CONSOLE_IDP_SCOPES"
29-
ConsoleIDPUserInfo = "CONSOLE_IDP_USERINFO"
30-
ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION"
21+
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
22+
ConsoleIDPURL = "CONSOLE_IDP_URL"
23+
ConsoleIDPClientID = "CONSOLE_IDP_CLIENT_ID"
24+
ConsoleIDPSecret = "CONSOLE_IDP_SECRET"
25+
ConsoleIDPCallbackURL = "CONSOLE_IDP_CALLBACK"
26+
ConsoleIDPCallbackURLDynamic = "CONSOLE_IDP_CALLBACK_DYNAMIC"
27+
ConsoleIDPHmacPassphrase = "CONSOLE_IDP_HMAC_PASSPHRASE"
28+
ConsoleIDPHmacSalt = "CONSOLE_IDP_HMAC_SALT"
29+
ConsoleIDPScopes = "CONSOLE_IDP_SCOPES"
30+
ConsoleIDPUserInfo = "CONSOLE_IDP_USERINFO"
31+
ConsoleIDPTokenExpiration = "CONSOLE_IDP_TOKEN_EXPIRATION"
3132
)

pkg/auth/idp/oauth2/provider.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,33 @@ var derivedKey = func() []byte {
119119
return pbkdf2.Key([]byte(getPassphraseForIDPHmac()), []byte(getSaltForIDPHmac()), 4096, 32, sha1.New)
120120
}
121121

122+
const (
123+
schemeHTTP = "http"
124+
schemeHTTPS = "https"
125+
)
126+
127+
func getLoginCallbackURL(r *http.Request) string {
128+
scheme := getSourceScheme(r)
129+
if scheme == "" {
130+
if r.TLS != nil {
131+
scheme = schemeHTTPS
132+
} else {
133+
scheme = schemeHTTP
134+
}
135+
}
136+
137+
redirectURL := scheme + "://" + r.Host + "/oauth_callback"
138+
_, err := url.Parse(redirectURL)
139+
if err != nil {
140+
panic(err)
141+
}
142+
return redirectURL
143+
}
144+
122145
// NewOauth2ProviderClient instantiates a new oauth2 client using the configured credentials
123146
// it returns a *Provider object that contains the necessary configuration to initiate an
124147
// oauth2 authentication flow
125-
func NewOauth2ProviderClient(scopes []string, httpClient *http.Client) (*Provider, error) {
126-
148+
func NewOauth2ProviderClient(scopes []string, r *http.Request, httpClient *http.Client) (*Provider, error) {
127149
ddoc, err := parseDiscoveryDoc(GetIDPURL(), httpClient)
128150
if err != nil {
129151
return nil, err
@@ -134,14 +156,21 @@ func NewOauth2ProviderClient(scopes []string, httpClient *http.Client) (*Provide
134156
scopes = strings.Split(getIDPScopes(), ",")
135157
}
136158

159+
redirectURL := GetIDPCallbackURL()
160+
if GetIDPCallbackURLDynamic() {
161+
// dynamic redirect if set, will generate redirect URLs
162+
// dynamically based on incoming requests.
163+
redirectURL = getLoginCallbackURL(r)
164+
}
165+
137166
// add "openid" scope always.
138167
scopes = append(scopes, "openid")
139168

140169
client := new(Provider)
141170
client.oauth2Config = &xoauth2.Config{
142171
ClientID: GetIDPClientID(),
143172
ClientSecret: GetIDPSecret(),
144-
RedirectURL: GetIDPCallbackURL(),
173+
RedirectURL: redirectURL,
145174
Endpoint: oauth2.Endpoint{
146175
AuthURL: ddoc.AuthEndpoint,
147176
TokenURL: ddoc.TokenEndpoint,

pkg/auth/idp/oauth2/proxy.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2021 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package oauth2
18+
19+
import (
20+
"net/http"
21+
"regexp"
22+
"strings"
23+
)
24+
25+
var (
26+
// De-facto standard header keys.
27+
xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
28+
xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme")
29+
)
30+
31+
var (
32+
// RFC7239 defines a new "Forwarded: " header designed to replace the
33+
// existing use of X-Forwarded-* headers.
34+
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
35+
forwarded = http.CanonicalHeaderKey("Forwarded")
36+
// Allows for a sub-match of the first value after 'for=' to the next
37+
// comma, semi-colon or space. The match is case-insensitive.
38+
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)(.*)`)
39+
// Allows for a sub-match for the first instance of scheme (http|https)
40+
// prefixed by 'proto='. The match is case-insensitive.
41+
protoRegex = regexp.MustCompile(`(?i)^(;|,| )+(?:proto=)(https|http)`)
42+
)
43+
44+
// getSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
45+
// Forwarded headers (in that order).
46+
func getSourceScheme(r *http.Request) string {
47+
var scheme string
48+
49+
// Retrieve the scheme from X-Forwarded-Proto.
50+
if proto := r.Header.Get(xForwardedProto); proto != "" {
51+
scheme = strings.ToLower(proto)
52+
} else if proto = r.Header.Get(xForwardedScheme); proto != "" {
53+
scheme = strings.ToLower(proto)
54+
} else if proto := r.Header.Get(forwarded); proto != "" {
55+
// match should contain at least two elements if the protocol was
56+
// specified in the Forwarded header. The first element will always be
57+
// the 'for=', which we ignore, subsequently we proceed to look for
58+
// 'proto=' which should precede right after `for=` if not
59+
// we simply ignore the values and return empty. This is in line
60+
// with the approach we took for returning first ip from multiple
61+
// params.
62+
if match := forRegex.FindStringSubmatch(proto); len(match) > 1 {
63+
if match = protoRegex.FindStringSubmatch(match[2]); len(match) > 1 {
64+
scheme = strings.ToLower(match[2])
65+
}
66+
}
67+
}
68+
69+
return scheme
70+
}

restapi/user_login.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import (
3838
func registerLoginHandlers(api *operations.ConsoleAPI) {
3939
// GET login strategy
4040
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
41-
loginDetails, err := getLoginDetailsResponse()
41+
loginDetails, err := getLoginDetailsResponse(params.HTTPRequest)
4242
if err != nil {
4343
return user_api.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
4444
}
@@ -59,7 +59,7 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
5959
})
6060
// POST login using external IDP
6161
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
62-
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
62+
loginResponse, err := getLoginOauth2AuthResponse(params.HTTPRequest, params.Body)
6363
if err != nil {
6464
return user_api.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
6565
}
@@ -131,14 +131,14 @@ func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.E
131131
}
132132

133133
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
134-
func getLoginDetailsResponse() (*models.LoginDetails, *models.Error) {
134+
func getLoginDetailsResponse(r *http.Request) (*models.LoginDetails, *models.Error) {
135135
loginStrategy := models.LoginDetailsLoginStrategyForm
136136
redirectURL := ""
137137

138138
if oauth2.IsIDPEnabled() {
139139
loginStrategy = models.LoginDetailsLoginStrategyRedirect
140140
// initialize new oauth2 client
141-
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, GetConsoleHTTPClient())
141+
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient())
142142
if err != nil {
143143
return nil, prepareError(err, errOauth2Provider)
144144
}
@@ -164,12 +164,12 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
164164
return userCredentials, nil
165165
}
166166

167-
func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
167+
func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, *models.Error) {
168168
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
169169
defer cancel()
170170
if oauth2.IsIDPEnabled() {
171171
// initialize new oauth2 client
172-
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, GetConsoleHTTPClient())
172+
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient())
173173
if err != nil {
174174
return nil, prepareError(err)
175175
}

0 commit comments

Comments
 (0)