github.com/snowflakedb/gosnowflake@v1.9.0/auth.go (about) 1 // Copyright (c) 2017-2022 Snowflake Computing Inc. All rights reserved. 2 3 package gosnowflake 4 5 import ( 6 "context" 7 "crypto/sha256" 8 "crypto/x509" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net/http" 14 "net/url" 15 "runtime" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/form3tech-oss/jwt-go" 21 ) 22 23 const ( 24 clientType = "Go" 25 ) 26 27 const ( 28 idToken = "ID_TOKEN" 29 mfaToken = "MFATOKEN" 30 clientStoreTemporaryCredential = "CLIENT_STORE_TEMPORARY_CREDENTIAL" 31 clientRequestMfaToken = "CLIENT_REQUEST_MFA_TOKEN" 32 idTokenAuthenticator = "ID_TOKEN" 33 ) 34 35 // AuthType indicates the type of authentication in Snowflake 36 type AuthType int 37 38 const ( 39 // AuthTypeSnowflake is the general username password authentication 40 AuthTypeSnowflake AuthType = iota 41 // AuthTypeOAuth is the OAuth authentication 42 AuthTypeOAuth 43 // AuthTypeExternalBrowser is to use a browser to access an Fed and perform SSO authentication 44 AuthTypeExternalBrowser 45 // AuthTypeOkta is to use a native okta URL to perform SSO authentication on Okta 46 AuthTypeOkta 47 // AuthTypeJwt is to use Jwt to perform authentication 48 AuthTypeJwt 49 // AuthTypeTokenAccessor is to use the provided token accessor and bypass authentication 50 AuthTypeTokenAccessor 51 // AuthTypeUsernamePasswordMFA is to use username and password with mfa 52 AuthTypeUsernamePasswordMFA 53 ) 54 55 func determineAuthenticatorType(cfg *Config, value string) error { 56 upperCaseValue := strings.ToUpper(value) 57 lowerCaseValue := strings.ToLower(value) 58 if strings.Trim(value, " ") == "" || upperCaseValue == AuthTypeSnowflake.String() { 59 cfg.Authenticator = AuthTypeSnowflake 60 return nil 61 } else if upperCaseValue == AuthTypeOAuth.String() { 62 cfg.Authenticator = AuthTypeOAuth 63 return nil 64 } else if upperCaseValue == AuthTypeJwt.String() { 65 cfg.Authenticator = AuthTypeJwt 66 return nil 67 } else if upperCaseValue == AuthTypeExternalBrowser.String() { 68 cfg.Authenticator = AuthTypeExternalBrowser 69 return nil 70 } else if upperCaseValue == AuthTypeUsernamePasswordMFA.String() { 71 cfg.Authenticator = AuthTypeUsernamePasswordMFA 72 return nil 73 } else if upperCaseValue == AuthTypeTokenAccessor.String() { 74 cfg.Authenticator = AuthTypeTokenAccessor 75 return nil 76 } else { 77 // possibly Okta case 78 oktaURLString, err := url.QueryUnescape(lowerCaseValue) 79 if err != nil { 80 return &SnowflakeError{ 81 Number: ErrCodeFailedToParseAuthenticator, 82 Message: errMsgFailedToParseAuthenticator, 83 MessageArgs: []interface{}{lowerCaseValue}, 84 } 85 } 86 87 oktaURL, err := url.Parse(oktaURLString) 88 if err != nil { 89 return &SnowflakeError{ 90 Number: ErrCodeFailedToParseAuthenticator, 91 Message: errMsgFailedToParseAuthenticator, 92 MessageArgs: []interface{}{oktaURLString}, 93 } 94 } 95 96 if oktaURL.Scheme != "https" || !strings.HasSuffix(oktaURL.Host, "okta.com") { 97 return &SnowflakeError{ 98 Number: ErrCodeFailedToParseAuthenticator, 99 Message: errMsgFailedToParseAuthenticator, 100 MessageArgs: []interface{}{oktaURLString}, 101 } 102 } 103 cfg.OktaURL = oktaURL 104 cfg.Authenticator = AuthTypeOkta 105 } 106 return nil 107 } 108 109 func (authType AuthType) String() string { 110 switch authType { 111 case AuthTypeSnowflake: 112 return "SNOWFLAKE" 113 case AuthTypeOAuth: 114 return "OAUTH" 115 case AuthTypeExternalBrowser: 116 return "EXTERNALBROWSER" 117 case AuthTypeOkta: 118 return "OKTA" 119 case AuthTypeJwt: 120 return "SNOWFLAKE_JWT" 121 case AuthTypeTokenAccessor: 122 return "TOKENACCESSOR" 123 case AuthTypeUsernamePasswordMFA: 124 return "USERNAME_PASSWORD_MFA" 125 default: 126 return "UNKNOWN" 127 } 128 } 129 130 // platform consists of compiler and architecture type in string 131 var platform = fmt.Sprintf("%v-%v", runtime.Compiler, runtime.GOARCH) 132 133 // operatingSystem is the runtime operating system. 134 var operatingSystem = runtime.GOOS 135 136 // userAgent shows up in User-Agent HTTP header 137 var userAgent = fmt.Sprintf("%v/%v (%v-%v) %v/%v", 138 clientType, 139 SnowflakeGoDriverVersion, 140 operatingSystem, 141 runtime.GOARCH, 142 runtime.Compiler, 143 runtime.Version()) 144 145 type authRequestClientEnvironment struct { 146 Application string `json:"APPLICATION"` 147 Os string `json:"OS"` 148 OsVersion string `json:"OS_VERSION"` 149 OCSPMode string `json:"OCSP_MODE"` 150 } 151 type authRequestData struct { 152 ClientAppID string `json:"CLIENT_APP_ID"` 153 ClientAppVersion string `json:"CLIENT_APP_VERSION"` 154 SvnRevision string `json:"SVN_REVISION"` 155 AccountName string `json:"ACCOUNT_NAME"` 156 LoginName string `json:"LOGIN_NAME,omitempty"` 157 Password string `json:"PASSWORD,omitempty"` 158 RawSAMLResponse string `json:"RAW_SAML_RESPONSE,omitempty"` 159 ExtAuthnDuoMethod string `json:"EXT_AUTHN_DUO_METHOD,omitempty"` 160 Passcode string `json:"PASSCODE,omitempty"` 161 Authenticator string `json:"AUTHENTICATOR,omitempty"` 162 SessionParameters map[string]interface{} `json:"SESSION_PARAMETERS,omitempty"` 163 ClientEnvironment authRequestClientEnvironment `json:"CLIENT_ENVIRONMENT"` 164 BrowserModeRedirectPort string `json:"BROWSER_MODE_REDIRECT_PORT,omitempty"` 165 ProofKey string `json:"PROOF_KEY,omitempty"` 166 Token string `json:"TOKEN,omitempty"` 167 } 168 type authRequest struct { 169 Data authRequestData `json:"data"` 170 } 171 172 type nameValueParameter struct { 173 Name string `json:"name"` 174 Value interface{} `json:"value"` 175 } 176 177 type authResponseSessionInfo struct { 178 DatabaseName string `json:"databaseName"` 179 SchemaName string `json:"schemaName"` 180 WarehouseName string `json:"warehouseName"` 181 RoleName string `json:"roleName"` 182 } 183 184 type authResponseMain struct { 185 Token string `json:"token,omitempty"` 186 Validity time.Duration `json:"validityInSeconds,omitempty"` 187 MasterToken string `json:"masterToken,omitempty"` 188 MasterValidity time.Duration `json:"masterValidityInSeconds"` 189 MfaToken string `json:"mfaToken,omitempty"` 190 MfaTokenValidity time.Duration `json:"mfaTokenValidityInSeconds"` 191 IDToken string `json:"idToken,omitempty"` 192 IDTokenValidity time.Duration `json:"idTokenValidityInSeconds"` 193 DisplayUserName string `json:"displayUserName"` 194 ServerVersion string `json:"serverVersion"` 195 FirstLogin bool `json:"firstLogin"` 196 RemMeToken string `json:"remMeToken"` 197 RemMeValidity time.Duration `json:"remMeValidityInSeconds"` 198 HealthCheckInterval time.Duration `json:"healthCheckInterval"` 199 NewClientForUpgrade string `json:"newClientForUpgrade"` 200 SessionID int64 `json:"sessionId"` 201 Parameters []nameValueParameter `json:"parameters"` 202 SessionInfo authResponseSessionInfo `json:"sessionInfo"` 203 TokenURL string `json:"tokenUrl,omitempty"` 204 SSOURL string `json:"ssoUrl,omitempty"` 205 ProofKey string `json:"proofKey,omitempty"` 206 } 207 208 type authResponse struct { 209 Data authResponseMain `json:"data"` 210 Message string `json:"message"` 211 Code string `json:"code"` 212 Success bool `json:"success"` 213 } 214 215 func postAuth( 216 ctx context.Context, 217 sr *snowflakeRestful, 218 client *http.Client, 219 params *url.Values, 220 headers map[string]string, 221 bodyCreator bodyCreatorType, 222 timeout time.Duration) ( 223 data *authResponse, err error) { 224 params.Add(requestIDKey, getOrGenerateRequestIDFromContext(ctx).String()) 225 params.Add(requestGUIDKey, NewUUID().String()) 226 227 fullURL := sr.getFullURL(loginRequestPath, params) 228 logger.Infof("full URL: %v", fullURL) 229 resp, err := sr.FuncAuthPost(ctx, client, fullURL, headers, bodyCreator, timeout, sr.MaxRetryCount) 230 if err != nil { 231 return nil, err 232 } 233 defer resp.Body.Close() 234 if resp.StatusCode == http.StatusOK { 235 var respd authResponse 236 err = json.NewDecoder(resp.Body).Decode(&respd) 237 if err != nil { 238 logger.Errorf("failed to decode JSON. err: %v", err) 239 return nil, err 240 } 241 return &respd, nil 242 } 243 switch resp.StatusCode { 244 case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout: 245 // service availability or connectivity issue. Most likely server side issue. 246 return nil, &SnowflakeError{ 247 Number: ErrCodeServiceUnavailable, 248 SQLState: SQLStateConnectionWasNotEstablished, 249 Message: errMsgServiceUnavailable, 250 MessageArgs: []interface{}{resp.StatusCode, fullURL}, 251 } 252 case http.StatusUnauthorized, http.StatusForbidden: 253 // failed to connect to db. account name may be wrong 254 return nil, &SnowflakeError{ 255 Number: ErrCodeFailedToConnect, 256 SQLState: SQLStateConnectionRejected, 257 Message: errMsgFailedToConnect, 258 MessageArgs: []interface{}{resp.StatusCode, fullURL}, 259 } 260 } 261 b, err := io.ReadAll(resp.Body) 262 if err != nil { 263 logger.Errorf("failed to extract HTTP response body. err: %v", err) 264 return nil, err 265 } 266 logger.Infof("HTTP: %v, URL: %v, Body: %v", resp.StatusCode, fullURL, b) 267 logger.Infof("Header: %v", resp.Header) 268 return nil, &SnowflakeError{ 269 Number: ErrFailedToAuth, 270 SQLState: SQLStateConnectionRejected, 271 Message: errMsgFailedToAuth, 272 MessageArgs: []interface{}{resp.StatusCode, fullURL}, 273 } 274 } 275 276 // Generates a map of headers needed to authenticate 277 // with Snowflake. 278 func getHeaders() map[string]string { 279 headers := make(map[string]string) 280 headers[httpHeaderContentType] = headerContentTypeApplicationJSON 281 headers[httpHeaderAccept] = headerAcceptTypeApplicationSnowflake 282 headers[httpClientAppID] = clientType 283 headers[httpClientAppVersion] = SnowflakeGoDriverVersion 284 headers[httpHeaderUserAgent] = userAgent 285 return headers 286 } 287 288 // Used to authenticate the user with Snowflake. 289 func authenticate( 290 ctx context.Context, 291 sc *snowflakeConn, 292 samlResponse []byte, 293 proofKey []byte, 294 ) (resp *authResponseMain, err error) { 295 if sc.cfg.Authenticator == AuthTypeTokenAccessor { 296 logger.Info("Bypass authentication using existing token from token accessor") 297 sessionInfo := authResponseSessionInfo{ 298 DatabaseName: sc.cfg.Database, 299 SchemaName: sc.cfg.Schema, 300 WarehouseName: sc.cfg.Warehouse, 301 RoleName: sc.cfg.Role, 302 } 303 token, masterToken, sessionID := sc.cfg.TokenAccessor.GetTokens() 304 return &authResponseMain{ 305 Token: token, 306 MasterToken: masterToken, 307 SessionID: sessionID, 308 SessionInfo: sessionInfo, 309 }, nil 310 } 311 312 headers := getHeaders() 313 clientEnvironment := authRequestClientEnvironment{ 314 Application: sc.cfg.Application, 315 Os: operatingSystem, 316 OsVersion: platform, 317 OCSPMode: sc.cfg.ocspMode(), 318 } 319 320 sessionParameters := make(map[string]interface{}) 321 paramsMutex.Lock() 322 for k, v := range sc.cfg.Params { 323 // upper casing to normalize keys 324 sessionParameters[strings.ToUpper(k)] = *v 325 } 326 paramsMutex.Unlock() 327 328 sessionParameters[sessionClientValidateDefaultParameters] = sc.cfg.ValidateDefaultParameters != ConfigBoolFalse 329 if sc.cfg.ClientRequestMfaToken == ConfigBoolTrue { 330 sessionParameters[clientRequestMfaToken] = true 331 } 332 if sc.cfg.ClientStoreTemporaryCredential == ConfigBoolTrue { 333 sessionParameters[clientStoreTemporaryCredential] = true 334 } 335 bodyCreator := func() ([]byte, error) { 336 return createRequestBody(sc, sessionParameters, clientEnvironment, proofKey, samlResponse) 337 } 338 339 params := &url.Values{} 340 if sc.cfg.Database != "" { 341 params.Add("databaseName", sc.cfg.Database) 342 } 343 if sc.cfg.Schema != "" { 344 params.Add("schemaName", sc.cfg.Schema) 345 } 346 if sc.cfg.Warehouse != "" { 347 params.Add("warehouse", sc.cfg.Warehouse) 348 } 349 if sc.cfg.Role != "" { 350 params.Add("roleName", sc.cfg.Role) 351 } 352 353 logger.WithContext(sc.ctx).Infof("PARAMS for Auth: %v, %v, %v, %v, %v, %v", 354 params, sc.rest.Protocol, sc.rest.Host, sc.rest.Port, sc.rest.LoginTimeout, sc.cfg.Authenticator.String()) 355 356 respd, err := sc.rest.FuncPostAuth(ctx, sc.rest, sc.rest.getClientFor(sc.cfg.Authenticator), params, headers, bodyCreator, sc.rest.LoginTimeout) 357 if err != nil { 358 return nil, err 359 } 360 if !respd.Success { 361 logger.Errorln("Authentication FAILED") 362 sc.rest.TokenAccessor.SetTokens("", "", -1) 363 if sessionParameters[clientRequestMfaToken] == true { 364 deleteCredential(sc, mfaToken) 365 } 366 if sessionParameters[clientStoreTemporaryCredential] == true { 367 deleteCredential(sc, idToken) 368 } 369 code, err := strconv.Atoi(respd.Code) 370 if err != nil { 371 code = -1 372 return nil, err 373 } 374 return nil, (&SnowflakeError{ 375 Number: code, 376 SQLState: SQLStateConnectionRejected, 377 Message: respd.Message, 378 }).exceptionTelemetry(sc) 379 } 380 logger.Info("Authentication SUCCESS") 381 sc.rest.TokenAccessor.SetTokens(respd.Data.Token, respd.Data.MasterToken, respd.Data.SessionID) 382 if sessionParameters[clientRequestMfaToken] == true { 383 token := respd.Data.MfaToken 384 setCredential(sc, mfaToken, token) 385 } 386 if sessionParameters[clientStoreTemporaryCredential] == true { 387 token := respd.Data.IDToken 388 setCredential(sc, idToken, token) 389 } 390 return &respd.Data, nil 391 } 392 393 func createRequestBody(sc *snowflakeConn, sessionParameters map[string]interface{}, 394 clientEnvironment authRequestClientEnvironment, proofKey []byte, samlResponse []byte, 395 ) ([]byte, error) { 396 requestMain := authRequestData{ 397 ClientAppID: clientType, 398 ClientAppVersion: SnowflakeGoDriverVersion, 399 AccountName: sc.cfg.Account, 400 SessionParameters: sessionParameters, 401 ClientEnvironment: clientEnvironment, 402 } 403 404 switch sc.cfg.Authenticator { 405 case AuthTypeExternalBrowser: 406 if sc.cfg.IDToken != "" { 407 requestMain.Authenticator = idTokenAuthenticator 408 requestMain.Token = sc.cfg.IDToken 409 requestMain.LoginName = sc.cfg.User 410 } else { 411 requestMain.ProofKey = string(proofKey) 412 requestMain.Token = string(samlResponse) 413 requestMain.LoginName = sc.cfg.User 414 requestMain.Authenticator = AuthTypeExternalBrowser.String() 415 } 416 case AuthTypeOAuth: 417 requestMain.LoginName = sc.cfg.User 418 requestMain.Authenticator = AuthTypeOAuth.String() 419 requestMain.Token = sc.cfg.Token 420 case AuthTypeOkta: 421 samlResponse, err := authenticateBySAML( 422 sc.ctx, 423 sc.rest, 424 sc.cfg.OktaURL, 425 sc.cfg.Application, 426 sc.cfg.Account, 427 sc.cfg.User, 428 sc.cfg.Password) 429 if err != nil { 430 return nil, err 431 } 432 requestMain.RawSAMLResponse = string(samlResponse) 433 case AuthTypeJwt: 434 requestMain.Authenticator = AuthTypeJwt.String() 435 436 jwtTokenString, err := prepareJWTToken(sc.cfg) 437 if err != nil { 438 return nil, err 439 } 440 requestMain.Token = jwtTokenString 441 case AuthTypeSnowflake: 442 logger.Info("Username and password") 443 requestMain.LoginName = sc.cfg.User 444 requestMain.Password = sc.cfg.Password 445 switch { 446 case sc.cfg.PasscodeInPassword: 447 requestMain.ExtAuthnDuoMethod = "passcode" 448 case sc.cfg.Passcode != "": 449 requestMain.Passcode = sc.cfg.Passcode 450 requestMain.ExtAuthnDuoMethod = "passcode" 451 } 452 case AuthTypeUsernamePasswordMFA: 453 logger.Info("Username and password MFA") 454 requestMain.LoginName = sc.cfg.User 455 requestMain.Password = sc.cfg.Password 456 if sc.cfg.MfaToken != "" { 457 requestMain.Token = sc.cfg.MfaToken 458 } 459 } 460 461 authRequest := authRequest{ 462 Data: requestMain, 463 } 464 jsonBody, err := json.Marshal(authRequest) 465 if err != nil { 466 return nil, err 467 } 468 return jsonBody, nil 469 } 470 471 // Generate a JWT token in string given the configuration 472 func prepareJWTToken(config *Config) (string, error) { 473 pubBytes, err := x509.MarshalPKIXPublicKey(config.PrivateKey.Public()) 474 if err != nil { 475 return "", err 476 } 477 hash := sha256.Sum256(pubBytes) 478 479 accountName := strings.ToUpper(config.Account) 480 userName := strings.ToUpper(config.User) 481 482 issueAtTime := time.Now().UTC() 483 token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ 484 "iss": fmt.Sprintf("%s.%s.%s", accountName, userName, "SHA256:"+base64.StdEncoding.EncodeToString(hash[:])), 485 "sub": fmt.Sprintf("%s.%s", accountName, userName), 486 "iat": issueAtTime.Unix(), 487 "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), 488 "exp": issueAtTime.Add(config.JWTExpireTimeout).Unix(), 489 }) 490 491 tokenString, err := token.SignedString(config.PrivateKey) 492 493 if err != nil { 494 return "", err 495 } 496 497 return tokenString, err 498 } 499 500 // Authenticate with sc.cfg 501 func authenticateWithConfig(sc *snowflakeConn) error { 502 var authData *authResponseMain 503 var samlResponse []byte 504 var proofKey []byte 505 var err error 506 //var consentCacheIdToken = true 507 508 if sc.cfg.Authenticator == AuthTypeExternalBrowser { 509 if (runtime.GOOS == "windows" || runtime.GOOS == "darwin") && sc.cfg.ClientStoreTemporaryCredential == configBoolNotSet { 510 sc.cfg.ClientStoreTemporaryCredential = ConfigBoolTrue 511 } 512 if sc.cfg.ClientStoreTemporaryCredential == ConfigBoolTrue { 513 fillCachedIDToken(sc) 514 } 515 // Disable console login by default 516 if sc.cfg.DisableConsoleLogin == configBoolNotSet { 517 sc.cfg.DisableConsoleLogin = ConfigBoolTrue 518 } 519 } 520 521 if sc.cfg.Authenticator == AuthTypeUsernamePasswordMFA { 522 if (runtime.GOOS == "windows" || runtime.GOOS == "darwin") && sc.cfg.ClientRequestMfaToken == configBoolNotSet { 523 sc.cfg.ClientRequestMfaToken = ConfigBoolTrue 524 } 525 if sc.cfg.ClientRequestMfaToken == ConfigBoolTrue { 526 fillCachedMfaToken(sc) 527 } 528 } 529 530 logger.Infof("Authenticating via %v", sc.cfg.Authenticator.String()) 531 switch sc.cfg.Authenticator { 532 case AuthTypeExternalBrowser: 533 if sc.cfg.IDToken == "" { 534 samlResponse, proofKey, err = authenticateByExternalBrowser( 535 sc.ctx, 536 sc.rest, 537 sc.cfg.Authenticator.String(), 538 sc.cfg.Application, 539 sc.cfg.Account, 540 sc.cfg.User, 541 sc.cfg.Password, 542 sc.cfg.ExternalBrowserTimeout, 543 sc.cfg.DisableConsoleLogin) 544 if err != nil { 545 sc.cleanup() 546 return err 547 } 548 } 549 } 550 authData, err = authenticate( 551 sc.ctx, 552 sc, 553 samlResponse, 554 proofKey) 555 if err != nil { 556 sc.cleanup() 557 return err 558 } 559 sc.populateSessionParameters(authData.Parameters) 560 sc.ctx = context.WithValue(sc.ctx, SFSessionIDKey, authData.SessionID) 561 return nil 562 } 563 564 func fillCachedIDToken(sc *snowflakeConn) { 565 getCredential(sc, idToken) 566 } 567 568 func fillCachedMfaToken(sc *snowflakeConn) { 569 getCredential(sc, mfaToken) 570 }