github.com/snowflakedb/gosnowflake@v1.9.0/authexternalbrowser.go (about) 1 // Copyright (c) 2019-2022 Snowflake Computing Inc. All rights reserved. 2 3 package gosnowflake 4 5 import ( 6 "bytes" 7 "context" 8 "encoding/base64" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "log" 14 "net" 15 "net/http" 16 "net/url" 17 "strconv" 18 "strings" 19 "time" 20 21 "github.com/pkg/browser" 22 ) 23 24 const ( 25 successHTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"/> 26 <title>SAML Response for Snowflake</title></head> 27 <body> 28 Your identity was confirmed and propagated to Snowflake %v. 29 You can close this window now and go back where you started from. 30 </body></html>` 31 ) 32 33 const ( 34 bufSize = 8192 35 ) 36 37 // Builds a response to show to the user after successfully 38 // getting a response from Snowflake. 39 func buildResponse(application string) bytes.Buffer { 40 body := fmt.Sprintf(successHTML, application) 41 t := &http.Response{ 42 Status: "200 OK", 43 StatusCode: 200, 44 Proto: "HTTP/1.1", 45 ProtoMajor: 1, 46 ProtoMinor: 1, 47 Body: io.NopCloser(bytes.NewBufferString(body)), 48 ContentLength: int64(len(body)), 49 Request: nil, 50 Header: make(http.Header), 51 } 52 var b bytes.Buffer 53 t.Write(&b) 54 return b 55 } 56 57 // This opens a socket that listens on all available unicast 58 // and any anycast IP addresses locally. By specifying "0", we are 59 // able to bind to a free port. 60 func createLocalTCPListener() (*net.TCPListener, error) { 61 l, err := net.Listen("tcp", "localhost:0") 62 if err != nil { 63 return nil, err 64 } 65 66 tcpListener, ok := l.(*net.TCPListener) 67 if !ok { 68 return nil, fmt.Errorf("failed to assert type as *net.TCPListener") 69 } 70 71 return tcpListener, nil 72 } 73 74 // Opens a browser window (or new tab) with the configured login Url. 75 // This can / will fail if running inside a shell with no display, ie 76 // ssh'ing into a box attempting to authenticate via external browser. 77 func openBrowser(loginURL string) error { 78 err := browser.OpenURL(loginURL) 79 if err != nil { 80 logger.Infof("failed to open a browser. err: %v", err) 81 return err 82 } 83 return nil 84 } 85 86 // Gets the IDP Url and Proof Key from Snowflake. 87 // Note: FuncPostAuthSaml will return a fully qualified error if 88 // there is something wrong getting data from Snowflake. 89 func getIdpURLProofKey( 90 ctx context.Context, 91 sr *snowflakeRestful, 92 authenticator string, 93 application string, 94 account string, 95 user string, 96 callbackPort int) (string, string, error) { 97 98 headers := make(map[string]string) 99 headers[httpHeaderContentType] = headerContentTypeApplicationJSON 100 headers[httpHeaderAccept] = headerContentTypeApplicationJSON 101 headers[httpHeaderUserAgent] = userAgent 102 103 clientEnvironment := authRequestClientEnvironment{ 104 Application: application, 105 Os: operatingSystem, 106 OsVersion: platform, 107 } 108 109 requestMain := authRequestData{ 110 ClientAppID: clientType, 111 ClientAppVersion: SnowflakeGoDriverVersion, 112 AccountName: account, 113 LoginName: user, 114 ClientEnvironment: clientEnvironment, 115 Authenticator: authenticator, 116 BrowserModeRedirectPort: strconv.Itoa(callbackPort), 117 } 118 119 authRequest := authRequest{ 120 Data: requestMain, 121 } 122 123 jsonBody, err := json.Marshal(authRequest) 124 if err != nil { 125 logger.WithContext(ctx).Errorf("failed to serialize json. err: %v", err) 126 return "", "", err 127 } 128 129 respd, err := sr.FuncPostAuthSAML(ctx, sr, headers, jsonBody, sr.LoginTimeout) 130 if err != nil { 131 return "", "", err 132 } 133 if !respd.Success { 134 logger.Errorln("Authentication FAILED") 135 sr.TokenAccessor.SetTokens("", "", -1) 136 code, err := strconv.Atoi(respd.Code) 137 if err != nil { 138 code = -1 139 return "", "", err 140 } 141 return "", "", &SnowflakeError{ 142 Number: code, 143 SQLState: SQLStateConnectionRejected, 144 Message: respd.Message, 145 } 146 } 147 return respd.Data.SSOURL, respd.Data.ProofKey, nil 148 } 149 150 // Gets the login URL for multiple SAML 151 func getLoginURL(sr *snowflakeRestful, user string, callbackPort int) (string, string, error) { 152 proofKey := generateProofKey() 153 154 params := &url.Values{} 155 params.Add("login_name", user) 156 params.Add("browser_mode_redirect_port", strconv.Itoa(callbackPort)) 157 params.Add("proof_key", proofKey) 158 url := sr.getFullURL(consoleLoginRequestPath, params) 159 160 return url.String(), proofKey, nil 161 } 162 163 func generateProofKey() string { 164 randomness := getSecureRandom(32) 165 return base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString(randomness) 166 } 167 168 // The response returned from Snowflake looks like so: 169 // GET /?token=encodedSamlToken 170 // Host: localhost:54001 171 // Connection: keep-alive 172 // Upgrade-Insecure-Requests: 1 173 // User-Agent: userAgentStr 174 // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 175 // Referer: https://myaccount.snowflakecomputing.com/fed/login 176 // Accept-Encoding: gzip, deflate, br 177 // Accept-Language: en-US,en;q=0.9 178 // This extracts the token portion of the response. 179 func getTokenFromResponse(response string) (string, error) { 180 start := "GET /?token=" 181 arr := strings.Split(response, "\r\n") 182 if !strings.HasPrefix(arr[0], start) { 183 logger.Errorf("response is malformed. ") 184 return "", &SnowflakeError{ 185 Number: ErrFailedToParseResponse, 186 SQLState: SQLStateConnectionRejected, 187 Message: errMsgFailedToParseResponse, 188 MessageArgs: []interface{}{response}, 189 } 190 } 191 token := strings.TrimPrefix(arr[0], start) 192 token = strings.Split(token, " ")[0] 193 return token, nil 194 } 195 196 type authenticateByExternalBrowserResult struct { 197 escapedSamlResponse []byte 198 proofKey []byte 199 err error 200 } 201 202 func authenticateByExternalBrowser( 203 ctx context.Context, 204 sr *snowflakeRestful, 205 authenticator string, 206 application string, 207 account string, 208 user string, 209 password string, 210 externalBrowserTimeout time.Duration, 211 disableConsoleLogin ConfigBool, 212 ) ([]byte, []byte, error) { 213 resultChan := make(chan authenticateByExternalBrowserResult, 1) 214 go func() { 215 resultChan <- doAuthenticateByExternalBrowser(ctx, sr, authenticator, application, account, user, password, disableConsoleLogin) 216 }() 217 select { 218 case <-time.After(externalBrowserTimeout): 219 return nil, nil, errors.New("authentication timed out") 220 case result := <-resultChan: 221 return result.escapedSamlResponse, result.proofKey, result.err 222 } 223 } 224 225 // Authentication by an external browser takes place via the following: 226 // - the golang snowflake driver communicates to Snowflake that the user wishes to 227 // authenticate via external browser 228 // - snowflake sends back the IDP Url configured at the Snowflake side for the 229 // provided account, or use the multiple SAML way via console login 230 // - the default browser is opened to that URL 231 // - user authenticates at the IDP, and is redirected to Snowflake 232 // - Snowflake directs the user back to the driver 233 // - authenticate is complete! 234 func doAuthenticateByExternalBrowser( 235 ctx context.Context, 236 sr *snowflakeRestful, 237 authenticator string, 238 application string, 239 account string, 240 user string, 241 password string, 242 disableConsoleLogin ConfigBool, 243 ) authenticateByExternalBrowserResult { 244 l, err := createLocalTCPListener() 245 if err != nil { 246 return authenticateByExternalBrowserResult{nil, nil, err} 247 } 248 defer l.Close() 249 250 callbackPort := l.Addr().(*net.TCPAddr).Port 251 252 var loginURL string 253 var proofKey string 254 if disableConsoleLogin == ConfigBoolTrue { 255 // Gets the IDP URL and Proof Key from Snowflake 256 loginURL, proofKey, err = getIdpURLProofKey(ctx, sr, authenticator, application, account, user, callbackPort) 257 } else { 258 // Multiple SAML way to do authentication via console login 259 loginURL, proofKey, err = getLoginURL(sr, user, callbackPort) 260 } 261 262 if err != nil { 263 return authenticateByExternalBrowserResult{nil, nil, err} 264 } 265 266 if err = openBrowser(loginURL); err != nil { 267 return authenticateByExternalBrowserResult{nil, nil, err} 268 } 269 270 encodedSamlResponseChan := make(chan string) 271 errChan := make(chan error) 272 273 var encodedSamlResponse string 274 var errFromGoroutine error 275 conn, err := l.Accept() 276 if err != nil { 277 logger.WithContext(ctx).Errorf("unable to accept connection. err: %v", err) 278 log.Fatal(err) 279 } 280 go func(c net.Conn) { 281 var buf bytes.Buffer 282 total := 0 283 encodedSamlResponse := "" 284 var errAccept error 285 for { 286 b := make([]byte, bufSize) 287 n, err := c.Read(b) 288 if err != nil { 289 if err != io.EOF { 290 logger.Infof("error reading from socket. err: %v", err) 291 errAccept = &SnowflakeError{ 292 Number: ErrFailedToGetExternalBrowserResponse, 293 SQLState: SQLStateConnectionRejected, 294 Message: errMsgFailedToGetExternalBrowserResponse, 295 MessageArgs: []interface{}{err}, 296 } 297 } 298 break 299 } 300 total += n 301 buf.Write(b) 302 if n < bufSize { 303 // We successfully read all data 304 s := string(buf.Bytes()[:total]) 305 encodedSamlResponse, errAccept = getTokenFromResponse(s) 306 break 307 } 308 buf.Grow(bufSize) 309 } 310 if encodedSamlResponse != "" { 311 httpResponse := buildResponse(application) 312 c.Write(httpResponse.Bytes()) 313 } 314 c.Close() 315 encodedSamlResponseChan <- encodedSamlResponse 316 errChan <- errAccept 317 }(conn) 318 319 encodedSamlResponse = <-encodedSamlResponseChan 320 errFromGoroutine = <-errChan 321 322 if errFromGoroutine != nil { 323 return authenticateByExternalBrowserResult{nil, nil, errFromGoroutine} 324 } 325 326 escapedSamlResponse, err := url.QueryUnescape(encodedSamlResponse) 327 if err != nil { 328 logger.WithContext(ctx).Errorf("unable to unescape saml response. err: %v", err) 329 return authenticateByExternalBrowserResult{nil, nil, err} 330 } 331 return authenticateByExternalBrowserResult{[]byte(escapedSamlResponse), []byte(proofKey), nil} 332 }