Allow multiple IDPs config to be passed via struct (#2167)
* Allow multiple IDPs config to be passed via struct * This removes support for ENV based IDP configuration for console * Ensure default scopes are used if none are given * Add display name field for provider config
This commit is contained in:
committed by
GitHub
parent
abb668633b
commit
118cf97e1d
@@ -98,7 +98,7 @@ func buildServer() (*restapi.Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api := operations.NewConsoleAPI(swaggerSpec, nil)
|
||||
api.Logger = restapi.LogInfo
|
||||
server := restapi.NewServer(api)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ func initConsoleServer() (*restapi.Server, error) {
|
||||
restapi.LogInfo = noLog
|
||||
restapi.LogError = noLog
|
||||
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api := operations.NewConsoleAPI(swaggerSpec, nil)
|
||||
api.Logger = noLog
|
||||
|
||||
server := restapi.NewServer(api)
|
||||
|
||||
@@ -25,6 +25,22 @@ import (
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
// ProviderConfig - OpenID IDP Configuration for console.
|
||||
type ProviderConfig struct {
|
||||
URL string
|
||||
DisplayName string // user-provided - can be empty
|
||||
ClientID, ClientSecret string
|
||||
HMACSalt, HMACPassphrase string
|
||||
Scopes string
|
||||
Userinfo bool
|
||||
RedirectCallbackDynamic bool
|
||||
RedirectCallback string
|
||||
}
|
||||
|
||||
type OpenIDPCfg map[string]ProviderConfig
|
||||
|
||||
var DefaultIDPConfig = "_"
|
||||
|
||||
func GetSTSEndpoint() string {
|
||||
return strings.TrimSpace(env.Get(ConsoleMinIOServer, "http://localhost:9000"))
|
||||
}
|
||||
|
||||
@@ -206,6 +206,79 @@ func NewOauth2ProviderClient(scopes []string, r *http.Request, httpClient *http.
|
||||
return client, nil
|
||||
}
|
||||
|
||||
var defaultScopes = []string{"openid", "profile", "email"}
|
||||
|
||||
// NewOauth2ProviderClient instantiates a new oauth2 client using the
|
||||
// `OpenIDPCfg` configuration struct. It returns a *Provider object that
|
||||
// contains the necessary configuration to initiate an oauth2 authentication
|
||||
// flow.
|
||||
//
|
||||
// We only support Authentication with the Authorization Code Flow - spec:
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
|
||||
func (o OpenIDPCfg) NewOauth2ProviderClient(name string, scopes []string, r *http.Request, httpClient *http.Client) (*Provider, error) {
|
||||
ddoc, err := parseDiscoveryDoc(o[name].URL, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supportedResponseTypes := set.NewStringSet()
|
||||
for _, responseType := range ddoc.ResponseTypesSupported {
|
||||
// FIXME: ResponseTypesSupported is a JSON array of strings - it
|
||||
// may not actually have strings with spaces inside them -
|
||||
// making the following code unnecessary.
|
||||
for _, s := range strings.Fields(responseType) {
|
||||
supportedResponseTypes.Add(s)
|
||||
}
|
||||
}
|
||||
isSupported := requiredResponseTypes.Difference(supportedResponseTypes).IsEmpty()
|
||||
|
||||
if !isSupported {
|
||||
return nil, fmt.Errorf("expected 'code' response type - got %s, login not allowed", ddoc.ResponseTypesSupported)
|
||||
}
|
||||
|
||||
// If provided scopes are empty we use the user configured list or a default
|
||||
// list.
|
||||
if len(scopes) == 0 {
|
||||
scopesTmp := strings.Split(o[name].Scopes, ",")
|
||||
for _, s := range scopesTmp {
|
||||
w := strings.TrimSpace(s)
|
||||
if w != "" {
|
||||
scopes = append(scopes, w)
|
||||
}
|
||||
}
|
||||
if len(scopes) == 0 {
|
||||
scopes = defaultScopes
|
||||
}
|
||||
}
|
||||
|
||||
redirectURL := o[name].RedirectCallback
|
||||
if o[name].RedirectCallbackDynamic {
|
||||
// dynamic redirect if set, will generate redirect URLs
|
||||
// dynamically based on incoming requests.
|
||||
redirectURL = getLoginCallbackURL(r)
|
||||
}
|
||||
|
||||
// add "openid" scope always.
|
||||
scopes = append(scopes, "openid")
|
||||
|
||||
client := new(Provider)
|
||||
client.oauth2Config = &xoauth2.Config{
|
||||
ClientID: o[name].ClientID,
|
||||
ClientSecret: o[name].ClientSecret,
|
||||
RedirectURL: redirectURL,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: ddoc.AuthEndpoint,
|
||||
TokenURL: ddoc.TokenEndpoint,
|
||||
},
|
||||
Scopes: scopes,
|
||||
}
|
||||
|
||||
client.ClientID = o[name].ClientID
|
||||
client.UserInfo = o[name].Userinfo
|
||||
client.provHTTPClient = httpClient
|
||||
return client, nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
AppMetadata map[string]interface{} `json:"app_metadata"`
|
||||
Blocked bool `json:"blocked"`
|
||||
|
||||
@@ -50,7 +50,7 @@ func initConsoleServer() (*restapi.Server, error) {
|
||||
restapi.LogInfo = noLog
|
||||
restapi.LogError = noLog
|
||||
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api := operations.NewConsoleAPI(swaggerSpec, nil)
|
||||
api.Logger = noLog
|
||||
|
||||
server := restapi.NewServer(api)
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestRegisterAdminArnsHandlers(t *testing.T) {
|
||||
if err != nil {
|
||||
assert.Fail("Error")
|
||||
}
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api := operations.NewConsoleAPI(swaggerSpec, nil)
|
||||
api.SystemArnListHandler = nil
|
||||
registerAdminArnsHandlers(api)
|
||||
if api.SystemArnListHandler == nil {
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/go-openapi/swag"
|
||||
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
"github.com/minio/console/restapi/operations/account"
|
||||
"github.com/minio/console/restapi/operations/auth"
|
||||
"github.com/minio/console/restapi/operations/bucket"
|
||||
@@ -58,7 +59,7 @@ import (
|
||||
)
|
||||
|
||||
// NewConsoleAPI creates a new Console instance
|
||||
func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
|
||||
func NewConsoleAPI(spec *loads.Document, openIDProviders oauth2.OpenIDPCfg) *ConsoleAPI {
|
||||
return &ConsoleAPI{
|
||||
handlers: make(map[string]map[string]http.Handler),
|
||||
formats: strfmt.Default,
|
||||
@@ -75,6 +76,8 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
|
||||
APIKeyAuthenticator: security.APIKeyAuth,
|
||||
BearerAuthenticator: security.BearerAuth,
|
||||
|
||||
OpenIDProviders: openIDProviders,
|
||||
|
||||
JSONConsumer: runtime.JSONConsumer(),
|
||||
MultipartformConsumer: runtime.DiscardConsumer,
|
||||
|
||||
@@ -478,6 +481,9 @@ type ConsoleAPI struct {
|
||||
Middleware func(middleware.Builder) http.Handler
|
||||
useSwaggerUI bool
|
||||
|
||||
// Configuration passed in from MinIO for MinIO console.
|
||||
OpenIDProviders oauth2.OpenIDPCfg
|
||||
|
||||
// BasicAuthenticator generates a runtime.Authenticator from the supplied basic auth function.
|
||||
// It has a default implementation in the security package, however you can replace it for your particular usage.
|
||||
BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
func registerLoginHandlers(api *operations.ConsoleAPI) {
|
||||
// GET login strategy
|
||||
api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder {
|
||||
loginDetails, err := getLoginDetailsResponse(params)
|
||||
loginDetails, err := getLoginDetailsResponse(params, api.OpenIDProviders, oauth2.DefaultIDPConfig)
|
||||
if err != nil {
|
||||
return authApi.NewLoginDetailDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
|
||||
})
|
||||
// POST login using external IDP
|
||||
api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder {
|
||||
loginResponse, err := getLoginOauth2AuthResponse(params)
|
||||
loginResponse, err := getLoginOauth2AuthResponse(params, api.OpenIDProviders, oauth2.DefaultIDPConfig)
|
||||
if err != nil {
|
||||
return authApi.NewLoginOauth2AuthDefault(int(err.Code)).WithPayload(err)
|
||||
}
|
||||
@@ -145,16 +145,16 @@ func getLoginResponse(params authApi.LoginParams) (*models.LoginResponse, *model
|
||||
}
|
||||
|
||||
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
|
||||
func getLoginDetailsResponse(params authApi.LoginDetailParams) (*models.LoginDetails, *models.Error) {
|
||||
func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders oauth2.OpenIDPCfg, idpName string) (*models.LoginDetails, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
loginStrategy := models.LoginDetailsLoginStrategyForm
|
||||
redirectURL := ""
|
||||
r := params.HTTPRequest
|
||||
if oauth2.IsIDPEnabled() {
|
||||
if openIDProviders != nil {
|
||||
loginStrategy = models.LoginDetailsLoginStrategyRedirect
|
||||
// initialize new oauth2 client
|
||||
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient())
|
||||
oauth2Client, err := openIDProviders.NewOauth2ProviderClient(idpName, nil, r, GetConsoleHTTPClient())
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err, ErrOauth2Provider)
|
||||
}
|
||||
@@ -180,14 +180,14 @@ func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI,
|
||||
return userCredentials, nil
|
||||
}
|
||||
|
||||
func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams) (*models.LoginResponse, *models.Error) {
|
||||
func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProviders oauth2.OpenIDPCfg, idpName string) (*models.LoginResponse, *models.Error) {
|
||||
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
|
||||
defer cancel()
|
||||
r := params.HTTPRequest
|
||||
lr := params.Body
|
||||
if oauth2.IsIDPEnabled() {
|
||||
if openIDProviders != nil {
|
||||
// initialize new oauth2 client
|
||||
oauth2Client, err := oauth2.NewOauth2ProviderClient(nil, r, GetConsoleHTTPClient())
|
||||
oauth2Client, err := openIDProviders.NewOauth2ProviderClient(idpName, nil, r, GetConsoleHTTPClient())
|
||||
if err != nil {
|
||||
return nil, ErrorWithContext(ctx, err)
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/minio/console/restapi/operations"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -40,10 +40,14 @@ var token string
|
||||
|
||||
func initConsoleServer(consoleIDPURL string) (*restapi.Server, error) {
|
||||
// Configure Console Server with vars to get the idp config from the container
|
||||
os.Setenv("CONSOLE_IDP_URL", consoleIDPURL)
|
||||
os.Setenv("CONSOLE_IDP_CLIENT_ID", "minio-client-app")
|
||||
os.Setenv("CONSOLE_IDP_SECRET", "minio-client-app-secret")
|
||||
os.Setenv("CONSOLE_IDP_CALLBACK", "http://127.0.0.1:9090/oauth_callback")
|
||||
pcfg := map[string]consoleoauth2.ProviderConfig{
|
||||
consoleoauth2.DefaultIDPConfig: {
|
||||
URL: consoleIDPURL,
|
||||
ClientID: "minio-client-app",
|
||||
ClientSecret: "minio-client-app-secret",
|
||||
RedirectCallback: "http://127.0.0.1:9090/oauth_callback",
|
||||
},
|
||||
}
|
||||
|
||||
swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON)
|
||||
if err != nil {
|
||||
@@ -58,7 +62,7 @@ func initConsoleServer(consoleIDPURL string) (*restapi.Server, error) {
|
||||
restapi.LogInfo = noLog
|
||||
restapi.LogError = noLog
|
||||
|
||||
api := operations.NewConsoleAPI(swaggerSpec)
|
||||
api := operations.NewConsoleAPI(swaggerSpec, pcfg)
|
||||
api.Logger = noLog
|
||||
|
||||
server := restapi.NewServer(api)
|
||||
@@ -246,5 +250,5 @@ func TestBadLogin(t *testing.T) {
|
||||
fmt.Println(response)
|
||||
fmt.Println(err)
|
||||
expectedError := response.Status
|
||||
assert.Equal("500 Internal Server Error", expectedError)
|
||||
assert.Equal("401 Unauthorized", expectedError)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user