github.com/minio/console@v1.4.1/api/user_login.go (about)

     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 api
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"encoding/json"
    23  	"fmt"
    24  	"net/http"
    25  	"strings"
    26  
    27  	"github.com/go-openapi/errors"
    28  
    29  	"github.com/go-openapi/runtime"
    30  	"github.com/go-openapi/runtime/middleware"
    31  	"github.com/minio/console/api/operations"
    32  	authApi "github.com/minio/console/api/operations/auth"
    33  	"github.com/minio/console/models"
    34  	"github.com/minio/console/pkg/auth"
    35  	"github.com/minio/console/pkg/auth/idp/oauth2"
    36  	"github.com/minio/madmin-go/v3"
    37  	"github.com/minio/minio-go/v7/pkg/credentials"
    38  	"github.com/minio/pkg/v3/env"
    39  )
    40  
    41  func registerLoginHandlers(api *operations.ConsoleAPI) {
    42  	// GET login strategy
    43  	api.AuthLoginDetailHandler = authApi.LoginDetailHandlerFunc(func(params authApi.LoginDetailParams) middleware.Responder {
    44  		loginDetails, err := getLoginDetailsResponse(params, GlobalMinIOConfig.OpenIDProviders)
    45  		if err != nil {
    46  			return authApi.NewLoginDetailDefault(err.Code).WithPayload(err.APIError)
    47  		}
    48  		return authApi.NewLoginDetailOK().WithPayload(loginDetails)
    49  	})
    50  	// POST login using user credentials
    51  	api.AuthLoginHandler = authApi.LoginHandlerFunc(func(params authApi.LoginParams) middleware.Responder {
    52  		loginResponse, err := getLoginResponse(params)
    53  		if err != nil {
    54  			return authApi.NewLoginDefault(err.Code).WithPayload(err.APIError)
    55  		}
    56  		// Custom response writer to set the session cookies
    57  		return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
    58  			cookie := NewSessionCookieForConsole(loginResponse.SessionID)
    59  			http.SetCookie(w, &cookie)
    60  			authApi.NewLoginNoContent().WriteResponse(w, p)
    61  		})
    62  	})
    63  	// POST login using external IDP
    64  	api.AuthLoginOauth2AuthHandler = authApi.LoginOauth2AuthHandlerFunc(func(params authApi.LoginOauth2AuthParams) middleware.Responder {
    65  		loginResponse, err := getLoginOauth2AuthResponse(params, GlobalMinIOConfig.OpenIDProviders)
    66  		if err != nil {
    67  			return authApi.NewLoginOauth2AuthDefault(err.Code).WithPayload(err.APIError)
    68  		}
    69  		// Custom response writer to set the session cookies
    70  		return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) {
    71  			cookie := NewSessionCookieForConsole(loginResponse.SessionID)
    72  			http.SetCookie(w, &cookie)
    73  			http.SetCookie(w, &http.Cookie{
    74  				Path:     "/",
    75  				Name:     "idp-refresh-token",
    76  				Value:    loginResponse.IDPRefreshToken,
    77  				HttpOnly: true,
    78  				Secure:   len(GlobalPublicCerts) > 0,
    79  				SameSite: http.SameSiteLaxMode,
    80  			})
    81  			authApi.NewLoginOauth2AuthNoContent().WriteResponse(w, p)
    82  		})
    83  	})
    84  }
    85  
    86  // login performs a check of ConsoleCredentials against MinIO, generates some claims and returns the jwt
    87  // for subsequent authentication
    88  func login(credentials ConsoleCredentialsI, sessionFeatures *auth.SessionFeatures) (*string, error) {
    89  	// try to obtain consoleCredentials,
    90  	tokens, err := credentials.Get()
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	// if we made it here, the consoleCredentials work, generate a jwt with claims
    96  	token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), sessionFeatures)
    97  	if err != nil {
    98  		LogError("error authenticating user: %v", err)
    99  		return nil, ErrInvalidLogin
   100  	}
   101  	return &token, nil
   102  }
   103  
   104  // getAccountInfo will return the current user information
   105  func getAccountInfo(ctx context.Context, client MinioAdmin) (*madmin.AccountInfo, error) {
   106  	accountInfo, err := client.AccountInfo(ctx)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	return &accountInfo, nil
   111  }
   112  
   113  // getConsoleCredentials will return ConsoleCredentials interface
   114  func getConsoleCredentials(accessKey, secretKey, clientIP string) (*ConsoleCredentials, error) {
   115  	creds, err := NewConsoleCredentials(accessKey, secretKey, GetMinIORegion(), clientIP)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	return &ConsoleCredentials{
   120  		ConsoleCredentials: creds,
   121  		AccountAccessKey:   accessKey,
   122  	}, nil
   123  }
   124  
   125  // getLoginResponse performs login() and serializes it to the handler's output
   126  func getLoginResponse(params authApi.LoginParams) (*models.LoginResponse, *CodedAPIError) {
   127  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   128  	defer cancel()
   129  	lr := params.Body
   130  	var err error
   131  	var consoleCreds *ConsoleCredentials
   132  	// if we receive an STS we use that instead of the credentials
   133  	if lr.Sts != "" {
   134  		creds := credentials.NewStaticV4(lr.AccessKey, lr.SecretKey, lr.Sts)
   135  		consoleCreds = &ConsoleCredentials{
   136  			ConsoleCredentials: creds,
   137  			AccountAccessKey:   lr.AccessKey,
   138  		}
   139  
   140  		credsVerificate, _ := creds.Get()
   141  
   142  		if credsVerificate.SessionToken == "" || credsVerificate.SecretAccessKey == "" || credsVerificate.AccessKeyID == "" {
   143  			return nil, ErrorWithContext(ctx, errors.New(401, "Invalid STS Params"))
   144  		}
   145  
   146  	} else {
   147  		clientIP := getClientIP(params.HTTPRequest)
   148  		// prepare console credentials
   149  		consoleCreds, err = getConsoleCredentials(lr.AccessKey, lr.SecretKey, clientIP)
   150  		if err != nil {
   151  			return nil, ErrorWithContext(ctx, err, ErrInvalidLogin)
   152  		}
   153  	}
   154  
   155  	sf := &auth.SessionFeatures{}
   156  	if lr.Features != nil {
   157  		sf.HideMenu = lr.Features.HideMenu
   158  	}
   159  	sessionID, err := login(consoleCreds, sf)
   160  	if err != nil {
   161  		return nil, ErrorWithContext(ctx, err, ErrInvalidLogin)
   162  	}
   163  	// serialize output
   164  	loginResponse := &models.LoginResponse{
   165  		SessionID: *sessionID,
   166  	}
   167  	return loginResponse, nil
   168  }
   169  
   170  // isKubernetes returns true if minio is running in kubernetes.
   171  func isKubernetes() bool {
   172  	// Kubernetes env used to validate if we are
   173  	// indeed running inside a kubernetes pod
   174  	// is KUBERNETES_SERVICE_HOST
   175  	// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L541
   176  	return env.Get("KUBERNETES_SERVICE_HOST", "") != ""
   177  }
   178  
   179  // getLoginDetailsResponse returns information regarding the Console authentication mechanism.
   180  func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders oauth2.OpenIDPCfg) (ld *models.LoginDetails, apiErr *CodedAPIError) {
   181  	loginStrategy := models.LoginDetailsLoginStrategyForm
   182  	var redirectRules []*models.RedirectRule
   183  
   184  	r := params.HTTPRequest
   185  
   186  	var loginDetails *models.LoginDetails
   187  	if len(openIDProviders) > 0 {
   188  		loginStrategy = models.LoginDetailsLoginStrategyRedirect
   189  	}
   190  
   191  	for name, provider := range openIDProviders {
   192  		// initialize new oauth2 client
   193  
   194  		oauth2Client, err := provider.GetOauth2Provider(name, nil, r, GetConsoleHTTPClient(getClientIP(params.HTTPRequest)))
   195  		if err != nil {
   196  			continue
   197  		}
   198  
   199  		// Validate user against IDP
   200  		identityProvider := &auth.IdentityProvider{
   201  			KeyFunc: provider.GetStateKeyFunc(),
   202  			Client:  oauth2Client,
   203  		}
   204  
   205  		displayName := fmt.Sprintf("Login with SSO (%s)", name)
   206  		serviceType := ""
   207  
   208  		if provider.DisplayName != "" {
   209  			displayName = provider.DisplayName
   210  		}
   211  
   212  		if provider.RoleArn != "" {
   213  			splitRoleArn := strings.Split(provider.RoleArn, ":")
   214  
   215  			if len(splitRoleArn) > 2 {
   216  				serviceType = splitRoleArn[2]
   217  			}
   218  		}
   219  
   220  		redirectRule := models.RedirectRule{
   221  			Redirect:    identityProvider.GenerateLoginURL(),
   222  			DisplayName: displayName,
   223  			ServiceType: serviceType,
   224  		}
   225  
   226  		redirectRules = append(redirectRules, &redirectRule)
   227  	}
   228  
   229  	if len(openIDProviders) > 0 && len(redirectRules) == 0 {
   230  		loginStrategy = models.LoginDetailsLoginStrategyForm
   231  		// No IDP configured fallback to username/password
   232  	}
   233  
   234  	loginDetails = &models.LoginDetails{
   235  		LoginStrategy: loginStrategy,
   236  		RedirectRules: redirectRules,
   237  		IsK8S:         isKubernetes(),
   238  		AnimatedLogin: getConsoleAnimatedLogin(),
   239  	}
   240  
   241  	return loginDetails, nil
   242  }
   243  
   244  // verifyUserAgainstIDP will verify user identity against the configured IDP and return MinIO credentials
   245  func verifyUserAgainstIDP(ctx context.Context, provider auth.IdentityProviderI, code, state string) (*credentials.Credentials, error) {
   246  	userCredentials, err := provider.VerifyIdentity(ctx, code, state)
   247  	if err != nil {
   248  		LogError("error validating user identity against idp: %v", err)
   249  		return nil, err
   250  	}
   251  	return userCredentials, nil
   252  }
   253  
   254  func getLoginOauth2AuthResponse(params authApi.LoginOauth2AuthParams, openIDProviders oauth2.OpenIDPCfg) (*models.LoginResponse, *CodedAPIError) {
   255  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   256  	defer cancel()
   257  	r := params.HTTPRequest
   258  	lr := params.Body
   259  
   260  	if len(openIDProviders) > 0 {
   261  		// we read state
   262  		rState := *lr.State
   263  
   264  		decodedRState, err := base64.StdEncoding.DecodeString(rState)
   265  		if err != nil {
   266  			return nil, ErrorWithContext(ctx, err)
   267  		}
   268  
   269  		var requestItems oauth2.LoginURLParams
   270  		if err = json.Unmarshal(decodedRState, &requestItems); err != nil {
   271  			return nil, ErrorWithContext(ctx, err)
   272  		}
   273  
   274  		IDPName := requestItems.IDPName
   275  		state := requestItems.State
   276  
   277  		providerCfg, ok := openIDProviders[IDPName]
   278  		if !ok {
   279  			return nil, ErrorWithContext(ctx, fmt.Errorf("selected IDP %s does not exist", IDPName))
   280  		}
   281  
   282  		// Initialize new identity provider with new oauth2Client per IDPName
   283  		oauth2Client, err := providerCfg.GetOauth2Provider(IDPName, nil, r,
   284  			GetConsoleHTTPClient(getClientIP(params.HTTPRequest)))
   285  		if err != nil {
   286  			return nil, ErrorWithContext(ctx, err)
   287  		}
   288  
   289  		identityProvider := auth.IdentityProvider{
   290  			KeyFunc: providerCfg.GetStateKeyFunc(),
   291  			Client:  oauth2Client,
   292  			RoleARN: providerCfg.RoleArn,
   293  		}
   294  		// Validate user against IDP
   295  		userCredentials, err := verifyUserAgainstIDP(ctx, identityProvider, *lr.Code, state)
   296  		if err != nil {
   297  			return nil, ErrorWithContext(ctx, err)
   298  		}
   299  		// initialize admin client
   300  		// login user against console and generate session token
   301  		token, err := login(&ConsoleCredentials{
   302  			ConsoleCredentials: userCredentials,
   303  			AccountAccessKey:   "",
   304  		}, nil)
   305  		if err != nil {
   306  			return nil, ErrorWithContext(ctx, err)
   307  		}
   308  		// serialize output
   309  		loginResponse := &models.LoginResponse{
   310  			SessionID:       *token,
   311  			IDPRefreshToken: identityProvider.Client.RefreshToken,
   312  		}
   313  		return loginResponse, nil
   314  	}
   315  	return nil, ErrorWithContext(ctx, ErrDefault)
   316  }