diff --git a/cmd/console/server.go b/cmd/console/server.go index 92ebbf732..83b745ed5 100644 --- a/cmd/console/server.go +++ b/cmd/console/server.go @@ -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) diff --git a/integration/buckets_test.go b/integration/buckets_test.go index 2e7fab3fd..d931d0b86 100644 --- a/integration/buckets_test.go +++ b/integration/buckets_test.go @@ -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) diff --git a/pkg/auth/idp/oauth2/config.go b/pkg/auth/idp/oauth2/config.go index df09d6948..b8920a643 100644 --- a/pkg/auth/idp/oauth2/config.go +++ b/pkg/auth/idp/oauth2/config.go @@ -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")) } diff --git a/pkg/auth/idp/oauth2/provider.go b/pkg/auth/idp/oauth2/provider.go index ac6b1b4a6..1689e49c7 100644 --- a/pkg/auth/idp/oauth2/provider.go +++ b/pkg/auth/idp/oauth2/provider.go @@ -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"` diff --git a/replication/replication_test.go b/replication/replication_test.go index 4aeacc5ad..903ed8010 100644 --- a/replication/replication_test.go +++ b/replication/replication_test.go @@ -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) diff --git a/restapi/admin_arns_test.go b/restapi/admin_arns_test.go index ce620ebe3..c00ce060c 100644 --- a/restapi/admin_arns_test.go +++ b/restapi/admin_arns_test.go @@ -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 { diff --git a/restapi/operations/console_api.go b/restapi/operations/console_api.go index b2d88574e..f4652e188 100644 --- a/restapi/operations/console_api.go +++ b/restapi/operations/console_api.go @@ -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 diff --git a/restapi/user_login.go b/restapi/user_login.go index 298caa4fb..7358353c0 100644 --- a/restapi/user_login.go +++ b/restapi/user_login.go @@ -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) } diff --git a/sso-integration/sso_test.go b/sso-integration/sso_test.go index 818da1d12..0aa714302 100644 --- a/sso-integration/sso_test.go +++ b/sso-integration/sso_test.go @@ -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) }