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 }