Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-955538: Multiple SAML Integrations Support #1025

Merged
merged 17 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,10 @@ func authenticateWithConfig(sc *snowflakeConn) error {
if sc.cfg.ClientStoreTemporaryCredential == ConfigBoolTrue {
fillCachedIDToken(sc)
}
// Disable console login by default
if sc.cfg.DisableConsoleLogin == configBoolNotSet {
sc.cfg.DisableConsoleLogin = ConfigBoolTrue
}
}

if sc.cfg.Authenticator == AuthTypeUsernamePasswordMFA {
Expand All @@ -524,7 +528,8 @@ func authenticateWithConfig(sc *snowflakeConn) error {
sc.cfg.Account,
sc.cfg.User,
sc.cfg.Password,
sc.cfg.ExternalBrowserTimeout)
sc.cfg.ExternalBrowserTimeout,
sc.cfg.DisableConsoleLogin)
if err != nil {
sc.cleanup()
return err
Expand Down
16 changes: 16 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,22 @@ func TestUnitAuthenticateWithConfigOkta(t *testing.T) {
assertEqualE(t, err.Error(), "failed to get SAML response")
}

func TestUnitAuthenticateWithConfigExternalBrowser(t *testing.T) {
var err error
sr := &snowflakeRestful{
FuncPostAuthSAML: postAuthSAMLError,
TokenAccessor: getSimpleTokenAccessor(),
}
sc := getDefaultSnowflakeConn()
sc.cfg.Authenticator = AuthTypeExternalBrowser
sc.cfg.ExternalBrowserTimeout = defaultExternalBrowserTimeout
sc.rest = sr
sc.ctx = context.Background()
err = authenticateWithConfig(sc)
assertNotNilF(t, err, "should have failed at FuncPostAuthSAML.")
assertEqualE(t, err.Error(), "failed to get SAML response")
}

func TestUnitAuthenticateExternalBrowser(t *testing.T) {
var err error
sr := &snowflakeRestful{
Expand Down
48 changes: 40 additions & 8 deletions authexternalbrowser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -70,11 +71,11 @@
return tcpListener, nil
}

// Opens a browser window (or new tab) with the configured IDP Url.
// Opens a browser window (or new tab) with the configured login Url.
// This can / will fail if running inside a shell with no display, ie
// ssh'ing into a box attempting to authenticate via external browser.
func openBrowser(idpURL string) error {
err := browser.OpenURL(idpURL)
func openBrowser(loginURL string) error {
err := browser.OpenURL(loginURL)

Check warning on line 78 in authexternalbrowser.go

View check run for this annotation

Codecov / codecov/patch

authexternalbrowser.go#L77-L78

Added lines #L77 - L78 were not covered by tests
if err != nil {
logger.Infof("failed to open a browser. err: %v", err)
return err
Expand All @@ -91,6 +92,7 @@
authenticator string,
application string,
account string,
user string,
callbackPort int) (string, string, error) {

headers := make(map[string]string)
Expand All @@ -108,6 +110,7 @@
ClientAppID: clientType,
ClientAppVersion: SnowflakeGoDriverVersion,
AccountName: account,
LoginName: user,
ClientEnvironment: clientEnvironment,
Authenticator: authenticator,
BrowserModeRedirectPort: strconv.Itoa(callbackPort),
Expand Down Expand Up @@ -144,6 +147,24 @@
return respd.Data.SSOURL, respd.Data.ProofKey, nil
}

// Gets the login URL for multiple SAML
func getLoginURL(sr *snowflakeRestful, user string, callbackPort int) (string, string, error) {
proofKey := generateProofKey()

params := &url.Values{}
params.Add("login_name", user)
params.Add("browser_mode_redirect_port", strconv.Itoa(callbackPort))
params.Add("proof_key", proofKey)
url := sr.getFullURL(consoleLoginRequestPath, params)

return url.String(), proofKey, nil

Check warning on line 160 in authexternalbrowser.go

View check run for this annotation

Codecov / codecov/patch

authexternalbrowser.go#L151-L160

Added lines #L151 - L160 were not covered by tests
}

func generateProofKey() string {
randomness := getSecureRandom(32)
return base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString(randomness)

Check warning on line 165 in authexternalbrowser.go

View check run for this annotation

Codecov / codecov/patch

authexternalbrowser.go#L163-L165

Added lines #L163 - L165 were not covered by tests
}

// The response returned from Snowflake looks like so:
// GET /?token=encodedSamlToken
// Host: localhost:54001
Expand Down Expand Up @@ -187,10 +208,11 @@
user string,
password string,
externalBrowserTimeout time.Duration,
disableConsoleLogin ConfigBool,
) ([]byte, []byte, error) {
resultChan := make(chan authenticateByExternalBrowserResult, 1)
go func() {
resultChan <- doAuthenticateByExternalBrowser(ctx, sr, authenticator, application, account, user, password)
resultChan <- doAuthenticateByExternalBrowser(ctx, sr, authenticator, application, account, user, password, disableConsoleLogin)
}()
select {
case <-time.After(externalBrowserTimeout):
Expand All @@ -204,7 +226,7 @@
// - the golang snowflake driver communicates to Snowflake that the user wishes to
// authenticate via external browser
// - snowflake sends back the IDP Url configured at the Snowflake side for the
// provided account
// provided account, or use the multiple SAML way via console login
// - the default browser is opened to that URL
// - user authenticates at the IDP, and is redirected to Snowflake
// - Snowflake directs the user back to the driver
Expand All @@ -217,6 +239,7 @@
account string,
user string,
password string,
disableConsoleLogin ConfigBool,
) authenticateByExternalBrowserResult {
l, err := createLocalTCPListener()
if err != nil {
Expand All @@ -225,13 +248,22 @@
defer l.Close()

callbackPort := l.Addr().(*net.TCPAddr).Port
idpURL, proofKey, err := getIdpURLProofKey(
ctx, sr, authenticator, application, account, callbackPort)

var loginURL string
var proofKey string
if disableConsoleLogin == ConfigBoolTrue {
// Gets the IDP URL and Proof Key from Snowflake
loginURL, proofKey, err = getIdpURLProofKey(ctx, sr, authenticator, application, account, user, callbackPort)
} else {
// Multiple SAML way to do authentication via console login
loginURL, proofKey, err = getLoginURL(sr, user, callbackPort)
}

Check warning on line 260 in authexternalbrowser.go

View check run for this annotation

Codecov / codecov/patch

authexternalbrowser.go#L258-L260

Added lines #L258 - L260 were not covered by tests

if err != nil {
return authenticateByExternalBrowserResult{nil, nil, err}
}

if err = openBrowser(idpURL); err != nil {
if err = openBrowser(loginURL); err != nil {

Check warning on line 266 in authexternalbrowser.go

View check run for this annotation

Codecov / codecov/patch

authexternalbrowser.go#L266

Added line #L266 was not covered by tests
return authenticateByExternalBrowserResult{nil, nil, err}
}

Expand Down
8 changes: 4 additions & 4 deletions authexternalbrowser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,17 @@ func TestUnitAuthenticateByExternalBrowser(t *testing.T) {
FuncPostAuthSAML: postAuthExternalBrowserError,
TokenAccessor: getSimpleTokenAccessor(),
}
_, _, err := authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout)
_, _, err := authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout, ConfigBoolTrue)
if err == nil {
t.Fatal("should have failed.")
}
sr.FuncPostAuthSAML = postAuthExternalBrowserFail
_, _, err = authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout)
_, _, err = authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout, ConfigBoolTrue)
if err == nil {
t.Fatal("should have failed.")
}
sr.FuncPostAuthSAML = postAuthExternalBrowserFailWithCode
_, _, err = authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout)
_, _, err = authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout, ConfigBoolTrue)
if err == nil {
t.Fatal("should have failed.")
}
Expand All @@ -128,7 +128,7 @@ func TestAuthenticationTimeout(t *testing.T) {
FuncPostAuthSAML: postAuthExternalBrowserError,
TokenAccessor: getSimpleTokenAccessor(),
}
_, _, err := authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout)
_, _, err := authenticateByExternalBrowser(context.Background(), sr, authenticator, application, account, user, password, timeout, ConfigBoolTrue)
if err.Error() != "authentication timed out" {
t.Fatal("should have timed out")
}
Expand Down
16 changes: 16 additions & 0 deletions dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
IncludeRetryReason ConfigBool // Should retried request contain retry reason

ClientConfigFile string // File path to the client configuration json file

DisableConsoleLogin ConfigBool // Indicates whether console login should be disabled
}

// Validate enables testing if config is correct.
Expand Down Expand Up @@ -262,6 +264,9 @@
if cfg.ClientConfigFile != "" {
params.Add("clientConfigFile", cfg.ClientConfigFile)
}
if cfg.DisableConsoleLogin != configBoolNotSet {
params.Add("disableConsoleLogin", strconv.FormatBool(cfg.DisableConsoleLogin != ConfigBoolFalse))
}

dsn = fmt.Sprintf("%v:%v@%v:%v", url.QueryEscape(cfg.User), url.QueryEscape(cfg.Password), cfg.Host, cfg.Port)
if params.Encode() != "" {
Expand Down Expand Up @@ -754,6 +759,17 @@
}
case "clientConfigFile":
cfg.ClientConfigFile = value
case "disableConsoleLogin":
var vv bool
vv, err = strconv.ParseBool(value)
if err != nil {
return
}

Check warning on line 767 in dsn.go

View check run for this annotation

Codecov / codecov/patch

dsn.go#L766-L767

Added lines #L766 - L767 were not covered by tests
if vv {
cfg.DisableConsoleLogin = ConfigBoolTrue
} else {
cfg.DisableConsoleLogin = ConfigBoolFalse
}
default:
if cfg.Params == nil {
cfg.Params = make(map[string]*string)
Expand Down
57 changes: 57 additions & 0 deletions dsn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,40 @@ func TestParseDSN(t *testing.T) {
dsn: "u:[email protected]:443?authenticator=http%3A%2F%2Fsc.okta.com&ocspFailOpen=true&validateDefaultParameters=true",
err: errFailedToParseAuthenticator(),
},
{
dsn: "u:[email protected]:9876?account=a&protocol=http&authenticator=EXTERNALBROWSER&disableConsoleLogin=true",
config: &Config{
Account: "a", User: "u", Password: "p",
Authenticator: AuthTypeExternalBrowser,
Protocol: "http", Host: "a.snowflake.local", Port: 9876,
OCSPFailOpen: OCSPFailOpenTrue,
ValidateDefaultParameters: ConfigBoolTrue,
ClientTimeout: defaultClientTimeout,
JWTClientTimeout: defaultJWTClientTimeout,
ExternalBrowserTimeout: defaultExternalBrowserTimeout,
IncludeRetryReason: ConfigBoolTrue,
DisableConsoleLogin: ConfigBoolTrue,
},
ocspMode: ocspModeFailOpen,
err: nil,
},
{
dsn: "u:[email protected]:9876?account=a&protocol=http&authenticator=EXTERNALBROWSER&disableConsoleLogin=false",
config: &Config{
Account: "a", User: "u", Password: "p",
Authenticator: AuthTypeExternalBrowser,
Protocol: "http", Host: "a.snowflake.local", Port: 9876,
OCSPFailOpen: OCSPFailOpenTrue,
ValidateDefaultParameters: ConfigBoolTrue,
ClientTimeout: defaultClientTimeout,
JWTClientTimeout: defaultJWTClientTimeout,
ExternalBrowserTimeout: defaultExternalBrowserTimeout,
IncludeRetryReason: ConfigBoolTrue,
DisableConsoleLogin: ConfigBoolFalse,
},
ocspMode: ocspModeFailOpen,
err: nil,
},
}

for _, at := range []AuthType{AuthTypeExternalBrowser, AuthTypeOAuth} {
Expand Down Expand Up @@ -873,6 +907,9 @@ func TestParseDSN(t *testing.T) {
if test.config.IncludeRetryReason != cfg.IncludeRetryReason {
t.Fatalf("%v: Failed to match IncludeRetryReason. expected: %v, got: %v", i, test.config.IncludeRetryReason, cfg.IncludeRetryReason)
}
if test.config.DisableConsoleLogin != cfg.DisableConsoleLogin {
t.Fatalf("%v: Failed to match DisableConsoleLogin. expected: %v, got: %v", i, test.config.DisableConsoleLogin, cfg.DisableConsoleLogin)
}
assertEqualF(t, cfg.ClientConfigFile, test.config.ClientConfigFile, "client config file")
case test.err != nil:
driverErrE, okE := test.err.(*SnowflakeError)
Expand Down Expand Up @@ -1322,6 +1359,26 @@ func TestDSN(t *testing.T) {
},
dsn: "u:[email protected]:443?clientConfigFile=c%3A%5CUsers%5Cuser%5Cconfig.json&ocspFailOpen=true&region=b.c&validateDefaultParameters=true",
},
{
cfg: &Config{
User: "u",
Password: "p",
Account: "a.b.c",
Authenticator: AuthTypeExternalBrowser,
DisableConsoleLogin: ConfigBoolTrue,
},
dsn: "u:[email protected]:443?authenticator=externalbrowser&disableConsoleLogin=true&ocspFailOpen=true&region=b.c&validateDefaultParameters=true",
},
{
cfg: &Config{
User: "u",
Password: "p",
Account: "a.b.c",
Authenticator: AuthTypeExternalBrowser,
DisableConsoleLogin: ConfigBoolFalse,
},
dsn: "u:[email protected]:443?authenticator=externalbrowser&disableConsoleLogin=false&ocspFailOpen=true&region=b.c&validateDefaultParameters=true",
},
}
for _, test := range testcases {
t.Run(test.dsn, func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions restful.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
monitoringQueriesPath = "/monitoring/queries"
sessionRequestPath = "/session"
heartBeatPath = "/session/heartbeat"
consoleLoginRequestPath = "/console/login"
)

type (
Expand Down
Loading