Repository: microsoft/go-mssqldb
Pull request #15 (2022-05-04 09:54:34)

Export auth interface to allow external implementations e.g. krb5 - V1-Candidate
The original driver auth interface and implementations have been moved into their own packages and exported. There are two implementations within the driver, NTLM and Windows SSPI.
As before the default on windows is the winsspi package and NTLM on Linux.

The consuming application can now override this at runtime by calling mssql.SetIntegratedAuthenticationProvider(authProvider) e.g.

// create a new auth.Provider around the kerberos client
provider := krb5.NewAuthProvider(krbClient)

// pass the provider to mssql to override the default authentication mechanism
mssql.SetIntegratedAuthenticationProvider(provider)
We have created an integratedauth.Provider implementation here https://github.com/bet365/go-mssqldb-auth-krb5 which uses [http://github.com/jcmturner/gokrb5](https://github.com/jcmturner/gokrb5) to authenticate with active directory.

This keep the main driver free from the krb5 dependency and opens the possibility for other implementations in future.

If this pull request is accepted we will update our implementation to the main repo master. Currently it is using a replacement to point at a branch.


PeteBassettBet365: Hi David. Sorry for the lateness of this reply, I've been on annual leave.

The 702 PR is quite different from our approach here but I think the base question is "Do you want to take a dependency on github.com/jcmturner/gokrb5 in go-mssqldb itself?"

As you say, ours is reasonably modular, the changes were only just enough to allow external packages to supply the required functionality at runtime, windows was already handled, we added linux. We had in mind a second repo which held the actual reference implementation which users could depend on if they required kerberos on linux. Our example implementation is here https://github.com/bet365/go-mssqldb-auth-krb5 but we dont expect it being the defacto implementation or repo. MS could have it if you like. We could extend it to show other common patterns quite easily, flesh it out.

Have you got a set of requirements in mind for this? Or any comments about the PR as it stands?
dergyitheron: Hi everyone, I've been occasionally watching [deniskom/#702](https://github.com/denisenkom/go-mssqldb/pull/702) and now it seems like this fork is on good way to finally make the krb5-based auth possible. 

From someone who simply uses the go-mssqldb module for many use cases, I like the modular approach as well but I think having working krb5 auth in this repo would help many much more, the mentioned PR has quite a lot of people supporting it.

In this particular need to have krb5-based auth, adding only auth interface to allow external implementations could lead to having different implementations in different packages or developers would have their own implementations that could later get outdated or loose maintenance workforce. 

I would support both the option to have external auth implementations and also having built-in krb5-based auth that would cover most of the use cases.
shueybubbles: thx for the input. 
This comment from 702 seems relevant: 
https://github.com/denisenkom/go-mssqldb/pull/702#issuecomment-1113844324

We are trying to move the azure identity dependency out of the core module altogether with #30, which makes the azuread package a separate module. That use case is a little different, as the app as to create an AAD-specific connector. 

For krb, it seems like it's not a big change for consumers of the driver to import two modules - the core mssql and a krb5 implementation. We'd be happy to host a krb5-based module in this repo, and it could be a version of the code from https://github.com/denisenkom/go-mssqldb/pull/702

With that in mind - what would it take to modify this PR to allow a module to automatically register itself as the krb auth implementation when an app imports it? Is it just a matter of the driver adding an init function that calls `mssql.SetIntegratedAuthenticationProvider` with an instance of itself?
chandanjainn: @shueybubbles can we move ahead and raise a PR cloning changes from https://github.com/denisenkom/go-mssqldb/pull/702 ? or are we looking for some modifications here? Would it be possible to take the changes for the modular approach as an enhancement later on ? 
PeteBassettBet365: Hi everyone. 

We're still working on the more modular approach here that was previously mentioned. 

Essentially it gives a default kerberos auth provider, on linux, but allows overriding. We are going to give the auth providers access to the parsed connection string so other implementations could make use of them. 

This would still keep krb5 out of the base driver, and would be an additional package used only on linux via an additional import.

One question I would have is if the 702 PR is restricted to just keytabs, or can raw credentials be used?
PeteBassettBet365: Hi. 

I've pushed a new set of changes for this that add, I believe, everything we wanted. 

We keep the krb5 packages separate from the main driver, expose a modular registration approach for authentication, change the interface into authentication to expose the whole connection string

The reference kerberos authentication implementation can be found here, as before. 
https://github.com/bet365/go-mssqldb-auth-krb5

It now supports three modes of auth. KeyTabs, Cached Credential Files and Direct Username & Password. 

All the values are specified on the connection string, including authentication provider.

I very much look forward to hearing any feedback you might have.
shueybubbles: @chandanjainn @stuartpa @PeteBassettBet365 Chandan is trying to implement kerb5 in this repo at #35 
I think Chandan's change could be adapted here by converting his additions to an integratedAuth provider. The question is - whose change should go in first? The second person would need to adapt their code to the first.
Chandan is already isolating most of his code in linux-specific files so moving that code around a bit shouldn't be too tricky.
PeteBassettBet365: With the modular approach, you can pick any kerberos implementation you like :-) I'll have a look at his in the morning.
codecov-commenter: # [Codecov](https://codecov.io/gh/microsoft/go-mssqldb/pull/15?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) Report
> Merging [#15](https://codecov.io/gh/microsoft/go-mssqldb/pull/15?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) (7fd8e3b) into [main](https://codecov.io/gh/microsoft/go-mssqldb/commit/2d408c3ae25a3af94a54322903b14c854e98b18c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) (2d408c3) will **increase** coverage by `0.43%`.
> The diff coverage is `57.14%`.

```diff
@@            Coverage Diff             @@
##             main      #15      +/-   ##
==========================================
+ Coverage   72.44%   72.88%   +0.43%     
==========================================
  Files          25       24       -1     
  Lines        5596     5447     -149     
==========================================
- Hits         4054     3970      -84     
+ Misses       1302     1236      -66     
- Partials      240      241       +1     
```


| [Impacted Files](https://codecov.io/gh/microsoft/go-mssqldb/pull/15?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft) | Coverage Δ | |
|---|---|---|
| [tds.go](https://codecov.io/gh/microsoft/go-mssqldb/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-dGRzLmdv) | `66.62% <37.50%> (-1.23%)` | :arrow_down: |
| [mssql.go](https://codecov.io/gh/microsoft/go-mssqldb/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-bXNzcWwuZ28=) | `85.78% <75.00%> (-0.47%)` | :arrow_down: |
| [auth\_windows.go](https://codecov.io/gh/microsoft/go-mssqldb/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-YXV0aF93aW5kb3dzLmdv) | `100.00% <100.00%> (ø)` | |
| [token.go](https://codecov.io/gh/microsoft/go-mssqldb/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-dG9rZW4uZ28=) | `64.29% <0.00%> (-0.59%)` | :arrow_down: |
| [integratedauth/winsspi/winsspi.go](https://codecov.io/gh/microsoft/go-mssqldb/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-aW50ZWdyYXRlZGF1dGgvd2luc3NwaS93aW5zc3BpLmdv) | | |
| [ucs22str\_386.go](https://codecov.io/gh/microsoft/go-mssqldb/pull/15/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft#diff-dWNzMjJzdHJfMzg2Lmdv) | | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=microsoft)


Patch:
diff --git a/appveyor.yml b/appveyor.yml
index 3de25a7..1912109 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -57,7 +57,7 @@ install:
   - go env
   - go get -u github.com/golang-sql/civil
   - go get -u github.com/golang-sql/sqlexp
-  
+  - go get -u golang.org/x/crypto/md4
 build_script:
   - go build
 
diff --git a/auth_unix.go b/auth_unix.go
new file mode 100644
index 0000000..79be775
--- /dev/null
+++ b/auth_unix.go
@@ -0,0 +1,15 @@
+// +build !windows
+
+package mssql
+
+import (
+	"github.com/microsoft/go-mssqldb/integratedauth"
+	// nolint importing the ntlm package causes it to be registered as an available authentication provider
+	_ "github.com/microsoft/go-mssqldb/integratedauth/ntlm"
+)
+
+func init() {
+	// we set the default authentication provider name here, rather than within each imported package,
+	// to force a known default. Go will order execution of init() calls but it is better to be explicit.
+	integratedauth.DefaultProviderName = "ntlm"
+}
diff --git a/auth_windows.go b/auth_windows.go
new file mode 100644
index 0000000..8ed454f
--- /dev/null
+++ b/auth_windows.go
@@ -0,0 +1,18 @@
+// +build windows
+
+package mssql
+
+import (
+	"github.com/microsoft/go-mssqldb/integratedauth"
+
+	// nolint importing the ntlm package causes it to be registered as an available authentication provider
+	_ "github.com/microsoft/go-mssqldb/integratedauth/ntlm"
+	// nolint importing the winsspi package causes it to be registered as an available authentication provider
+	_ "github.com/microsoft/go-mssqldb/integratedauth/winsspi"
+)
+
+func init() {
+	// we set the default authentication provider name here, rather than within each imported package,
+	// to force a known default. Go will order execution of init() calls but it is better to be explicit.
+	integratedauth.DefaultProviderName = "winsspi"
+}
diff --git a/azuread/configuration.go b/azuread/configuration.go
index 041c0ef..e3c2eb7 100644
--- a/azuread/configuration.go
+++ b/azuread/configuration.go
@@ -53,7 +53,7 @@ type azureFedAuthConfig struct {
 
 // parse returns a config based on an msdsn-style connection string
 func parse(dsn string) (*azureFedAuthConfig, error) {
-	mssqlConfig, params, err := msdsn.Parse(dsn)
+	mssqlConfig, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
@@ -62,7 +62,7 @@ func parse(dsn string) (*azureFedAuthConfig, error) {
 		mssqlConfig:    mssqlConfig,
 	}
 
-	err = config.validateParameters(params)
+	err = config.validateParameters(mssqlConfig.Parameters)
 	if err != nil {
 		return nil, err
 	}
diff --git a/integratedauth/auth.go b/integratedauth/auth.go
new file mode 100644
index 0000000..0d68da7
--- /dev/null
+++ b/integratedauth/auth.go
@@ -0,0 +1,73 @@
+package integratedauth
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+var (
+	providers           map[string]Provider
+	DefaultProviderName string
+
+	ErrProviderCannotBeNil         = errors.New("provider cannot be nil")
+	ErrProviderNameMustBePopulated = errors.New("provider name must be populated")
+)
+
+func init() {
+	providers = make(map[string]Provider)
+}
+
+// GetIntegratedAuthenticator calls the authProvider specified in the 'authenticator' connection string parameter, if supplied.
+// Otherwise fails back to the DefaultProviderName implementation for the platform.
+func GetIntegratedAuthenticator(config msdsn.Config) (IntegratedAuthenticator, error) {
+	authenticatorName, ok := config.Parameters["authenticator"]
+	if !ok {
+		provider, err := getProvider(DefaultProviderName)
+		if err != nil {
+			return nil, err
+		}
+
+		p, err := provider.GetIntegratedAuthenticator(config)
+		// we ignore the error in this case to force a fallback to sqlserver authentication.
+		// this preserves the original behaviour
+		if err != nil {
+			return nil, nil
+		}
+
+		return p, nil
+	}
+
+	provider, err := getProvider(authenticatorName)
+	if err != nil {
+		return nil, err
+	}
+
+	return provider.GetIntegratedAuthenticator(config)
+}
+
+func getProvider(name string) (Provider, error) {
+	provider, ok := providers[name]
+
+	if !ok {
+		return nil, fmt.Errorf("provider %v not found", name)
+	}
+
+	return provider, nil
+}
+
+// SetIntegratedAuthenticationProvider stores a named authentication provider. It should be called before any connections are created.
+func SetIntegratedAuthenticationProvider(providerName string, p Provider) error {
+	if p == nil {
+		return ErrProviderCannotBeNil
+	}
+
+	if providerName == "" {
+		return ErrProviderNameMustBePopulated
+	}
+
+	providers[providerName] = p
+
+	return nil
+}
diff --git a/integratedauth/integratedauthenticator.go b/integratedauth/integratedauthenticator.go
new file mode 100644
index 0000000..ce8240d
--- /dev/null
+++ b/integratedauth/integratedauthenticator.go
@@ -0,0 +1,25 @@
+package integratedauth
+
+import (
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+// Provider returns an SSPI compatible authentication provider
+type Provider interface {
+	// GetIntegratedAuthenticator is responsible for returning an instance of the required IntegratedAuthenticator interface
+	GetIntegratedAuthenticator(config msdsn.Config) (IntegratedAuthenticator, error)
+}
+
+// IntegratedAuthenticator is the interface for SSPI Login Authentication providers
+type IntegratedAuthenticator interface {
+	InitialBytes() ([]byte, error)
+	NextBytes([]byte) ([]byte, error)
+	Free()
+}
+
+// ProviderFunc is an adapter to convert a GetIntegratedAuthenticator func into a Provider
+type ProviderFunc func(config msdsn.Config) (IntegratedAuthenticator, error)
+
+func (f ProviderFunc) GetIntegratedAuthenticator(config msdsn.Config) (IntegratedAuthenticator, error) {
+	return f(config)
+}
diff --git a/ntlm.go b/integratedauth/ntlm/ntlm.go
similarity index 94%
rename from ntlm.go
rename to integratedauth/ntlm/ntlm.go
index 90adb5a..d95032f 100644
--- a/ntlm.go
+++ b/integratedauth/ntlm/ntlm.go
@@ -1,6 +1,4 @@
-// +build !windows
-
-package mssql
+package ntlm
 
 import (
 	"crypto/des"
@@ -14,6 +12,9 @@ import (
 	"time"
 	"unicode/utf16"
 
+	"github.com/microsoft/go-mssqldb/integratedauth"
+	"github.com/microsoft/go-mssqldb/msdsn"
+
 	//lint:ignore SA1019 MD4 is used by legacy NTLM
 	"golang.org/x/crypto/md4"
 )
@@ -56,24 +57,26 @@ const _NEGOTIATE_FLAGS = _NEGOTIATE_UNICODE |
 	_NEGOTIATE_ALWAYS_SIGN |
 	_NEGOTIATE_EXTENDED_SESSIONSECURITY
 
-type ntlmAuth struct {
+type Auth struct {
 	Domain      string
 	UserName    string
 	Password    string
 	Workstation string
 }
 
-func getAuth(user, password, service, workstation string) (auth, bool) {
-	if !strings.ContainsRune(user, '\\') {
-		return nil, false
+// getAuth returns an authentication handle Auth to provide authentication content
+// to mssql.connect
+func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error) {
+	if !strings.ContainsRune(config.User, '\\') {
+		return nil, fmt.Errorf("ntlm : invalid username %v", config.User)
 	}
-	domain_user := strings.SplitN(user, "\\", 2)
-	return &ntlmAuth{
-		Domain:      domain_user[0],
-		UserName:    domain_user[1],
-		Password:    password,
-		Workstation: workstation,
-	}, true
+	domainUser := strings.SplitN(config.User, "\\", 2)
+	return &Auth{
+		Domain:      domainUser[0],
+		UserName:    domainUser[1],
+		Password:    config.Password,
+		Workstation: config.Workstation,
+	}, nil
 }
 
 func utf16le(val string) []byte {
@@ -90,7 +93,7 @@ func utf16le(val string) []byte {
 	return v
 }
 
-func (auth *ntlmAuth) InitialBytes() ([]byte, error) {
+func (auth *Auth) InitialBytes() ([]byte, error) {
 	domain_len := len(auth.Domain)
 	workstation_len := len(auth.Workstation)
 	msg := make([]byte, 40+domain_len+workstation_len)
@@ -358,7 +361,7 @@ func buildNTLMResponsePayload(lm, nt []byte, flags uint32, domain, workstation,
 	return msg, nil
 }
 
-func (auth *ntlmAuth) NextBytes(bytes []byte) ([]byte, error) {
+func (auth *Auth) NextBytes(bytes []byte) ([]byte, error) {
 	signature := string(bytes[0:8])
 	if signature != "NTLMSSP\x00" {
 		return nil, errorNTLM
@@ -389,5 +392,5 @@ func (auth *ntlmAuth) NextBytes(bytes []byte) ([]byte, error) {
 	return buildNTLMResponsePayload(lm, nt, flags, auth.Domain, auth.Workstation, auth.UserName)
 }
 
-func (auth *ntlmAuth) Free() {
+func (auth *Auth) Free() {
 }
diff --git a/integratedauth/ntlm/provider.go b/integratedauth/ntlm/provider.go
new file mode 100644
index 0000000..b0c780b
--- /dev/null
+++ b/integratedauth/ntlm/provider.go
@@ -0,0 +1,15 @@
+package ntlm
+
+import (
+	"github.com/microsoft/go-mssqldb/integratedauth"
+)
+
+// AuthProvider handles NTLM SSPI Windows Authentication
+var AuthProvider integratedauth.Provider = integratedauth.ProviderFunc(getAuth)
+
+func init() {
+	err := integratedauth.SetIntegratedAuthenticationProvider("ntlm", AuthProvider)
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/integratedauth/winsspi/provider.go b/integratedauth/winsspi/provider.go
new file mode 100644
index 0000000..05da93f
--- /dev/null
+++ b/integratedauth/winsspi/provider.go
@@ -0,0 +1,15 @@
+// +build windows
+
+package winsspi
+
+import "github.com/microsoft/go-mssqldb/integratedauth"
+
+// AuthProvider handles SSPI Windows Authentication via secur32.dll functions
+var AuthProvider integratedauth.Provider = integratedauth.ProviderFunc(getAuth)
+
+func init() {
+	err := integratedauth.SetIntegratedAuthenticationProvider("winsspi", AuthProvider)
+	if err != nil {
+		panic(err)
+	}
+}
\ No newline at end of file
diff --git a/sspi_windows.go b/integratedauth/winsspi/winsspi.go
similarity index 88%
rename from sspi_windows.go
rename to integratedauth/winsspi/winsspi.go
index 9b5bc68..195d228 100644
--- a/sspi_windows.go
+++ b/integratedauth/winsspi/winsspi.go
@@ -1,10 +1,15 @@
-package mssql
+// +build windows
+
+package winsspi
 
 import (
 	"fmt"
 	"strings"
 	"syscall"
 	"unsafe"
+
+	"github.com/microsoft/go-mssqldb/integratedauth"
+	"github.com/microsoft/go-mssqldb/msdsn"
 )
 
 var (
@@ -104,7 +109,7 @@ type SecBufferDesc struct {
 	pBuffers  *SecBuffer
 }
 
-type SSPIAuth struct {
+type Auth struct {
 	Domain   string
 	UserName string
 	Password string
@@ -113,23 +118,25 @@ type SSPIAuth struct {
 	ctxt     SecHandle
 }
 
-func getAuth(user, password, service, workstation string) (auth, bool) {
-	if user == "" {
-		return &SSPIAuth{Service: service}, true
+// getAuth returns an authentication handle Auth to provide authentication content
+// to mssql.connect
+func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error) {
+	if config.User == "" {
+		return &Auth{Service: config.ServerSPN}, nil
 	}
-	if !strings.ContainsRune(user, '\\') {
-		return nil, false
+	if !strings.ContainsRune(config.User, '\\') {
+		return nil, fmt.Errorf("winsspi : invalid username %v", config.User)
 	}
-	domain_user := strings.SplitN(user, "\\", 2)
-	return &SSPIAuth{
-		Domain:   domain_user[0],
-		UserName: domain_user[1],
-		Password: password,
-		Service:  service,
-	}, true
+	domainUser := strings.SplitN(config.User, "\\", 2)
+	return &Auth{
+		Domain:   domainUser[0],
+		UserName: domainUser[1],
+		Password: config.Password,
+		Service:  config.ServerSPN,
+	}, nil
 }
 
-func (auth *SSPIAuth) InitialBytes() ([]byte, error) {
+func (auth *Auth) InitialBytes() ([]byte, error) {
 	var identity *SEC_WINNT_AUTH_IDENTITY
 	if auth.UserName != "" {
 		identity = &SEC_WINNT_AUTH_IDENTITY{
@@ -202,7 +209,7 @@ func (auth *SSPIAuth) InitialBytes() ([]byte, error) {
 	return outbuf[:buf.cbBuffer], nil
 }
 
-func (auth *SSPIAuth) NextBytes(bytes []byte) ([]byte, error) {
+func (auth *Auth) NextBytes(bytes []byte) ([]byte, error) {
 	var in_buf, out_buf SecBuffer
 	var in_desc, out_desc SecBufferDesc
 
@@ -254,7 +261,7 @@ func (auth *SSPIAuth) NextBytes(bytes []byte) ([]byte, error) {
 	return outbuf[:out_buf.cbBuffer], nil
 }
 
-func (auth *SSPIAuth) Free() {
+func (auth *Auth) Free() {
 	syscall.Syscall6(sec_fn.DeleteSecurityContext,
 		1,
 		uintptr(unsafe.Pointer(&auth.ctxt)),
diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go
index 0b5354c..74799ee 100644
--- a/msdsn/conn_str.go
+++ b/msdsn/conn_str.go
@@ -73,6 +73,8 @@ type Config struct {
 	ConnTimeout time.Duration // Use context for timeouts.
 	KeepAlive   time.Duration // Leave at default.
 	PacketSize  uint16
+
+	Parameters map[string]string
 }
 
 func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string) (*tls.Config, error) {
@@ -109,31 +111,32 @@ func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate str
 
 var skipSetup = errors.New("skip setting up TLS")
 
-func Parse(dsn string) (Config, map[string]string, error) {
+func Parse(dsn string) (Config, error) {
 	p := Config{}
 
 	var params map[string]string
+	var err error
 	if strings.HasPrefix(dsn, "odbc:") {
-		parameters, err := splitConnectionStringOdbc(dsn[len("odbc:"):])
+		params, err = splitConnectionStringOdbc(dsn[len("odbc:"):])
 		if err != nil {
-			return p, params, err
+			return p, err
 		}
-		params = parameters
 	} else if strings.HasPrefix(dsn, "sqlserver://") {
-		parameters, err := splitConnectionStringURL(dsn)
+		params, err = splitConnectionStringURL(dsn)
 		if err != nil {
-			return p, params, err
+			return p, err
 		}
-		params = parameters
 	} else {
 		params = splitConnectionString(dsn)
 	}
 
+	p.Parameters = params
+
 	strlog, ok := params["log"]
 	if ok {
 		flags, err := strconv.ParseUint(strlog, 10, 64)
 		if err != nil {
-			return p, params, fmt.Errorf("invalid log parameter '%s': %s", strlog, err.Error())
+			return p, fmt.Errorf("invalid log parameter '%s': %s", strlog, err.Error())
 		}
 		p.LogFlags = Log(flags)
 	}
@@ -157,7 +160,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		p.Port, err = strconv.ParseUint(strport, 10, 16)
 		if err != nil {
 			f := "invalid tcp port '%v': %v"
-			return p, params, fmt.Errorf(f, strport, err.Error())
+			return p, fmt.Errorf(f, strport, err.Error())
 		}
 	}
 
@@ -168,7 +171,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		psize, err := strconv.ParseUint(strpsize, 0, 16)
 		if err != nil {
 			f := "invalid packet size '%v': %v"
-			return p, params, fmt.Errorf(f, strpsize, err.Error())
+			return p, fmt.Errorf(f, strpsize, err.Error())
 		}
 
 		// Ensure packet size falls within the TDS protocol range of 512 to 32767 bytes
@@ -191,7 +194,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		timeout, err := strconv.ParseUint(strconntimeout, 10, 64)
 		if err != nil {
 			f := "invalid connection timeout '%v': %v"
-			return p, params, fmt.Errorf(f, strconntimeout, err.Error())
+			return p, fmt.Errorf(f, strconntimeout, err.Error())
 		}
 		p.ConnTimeout = time.Duration(timeout) * time.Second
 	}
@@ -200,7 +203,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		timeout, err := strconv.ParseUint(strdialtimeout, 10, 64)
 		if err != nil {
 			f := "invalid dial timeout '%v': %v"
-			return p, params, fmt.Errorf(f, strdialtimeout, err.Error())
+			return p, fmt.Errorf(f, strdialtimeout, err.Error())
 		}
 		p.DialTimeout = time.Duration(timeout) * time.Second
 	}
@@ -212,7 +215,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		timeout, err := strconv.ParseUint(keepAlive, 10, 64)
 		if err != nil {
 			f := "invalid keepAlive value '%s': %s"
-			return p, params, fmt.Errorf(f, keepAlive, err.Error())
+			return p, fmt.Errorf(f, keepAlive, err.Error())
 		}
 		p.KeepAlive = time.Duration(timeout) * time.Second
 	}
@@ -230,7 +233,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 			e, err := strconv.ParseBool(encrypt)
 			if err != nil {
 				f := "invalid encrypt '%s': %s"
-				return p, params, fmt.Errorf(f, encrypt, err.Error())
+				return p, fmt.Errorf(f, encrypt, err.Error())
 			}
 			if e {
 				p.Encryption = EncryptionRequired
@@ -245,7 +248,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		trustServerCert, err = strconv.ParseBool(trust)
 		if err != nil {
 			f := "invalid trust server certificate '%s': %s"
-			return p, params, fmt.Errorf(f, trust, err.Error())
+			return p, fmt.Errorf(f, trust, err.Error())
 		}
 	}
 	certificate = params["certificate"]
@@ -262,7 +265,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		var err error
 		p.TLSConfig, err = SetupTLS(certificate, trustServerCert, hostInCertificate, tlsMin)
 		if err != nil {
-			return p, params, fmt.Errorf("failed to setup TLS: %w", err)
+			return p, fmt.Errorf("failed to setup TLS: %w", err)
 		}
 	}
 
@@ -270,7 +273,8 @@ func Parse(dsn string) (Config, map[string]string, error) {
 	if ok {
 		p.ServerSPN = serverSPN
 	} else {
-		p.ServerSPN = generateSpn(p.Host, p.Port)
+		// allow connections to sql server instances
+		p.ServerSPN = generateSpn(p.Host, instanceOrPort(p.Instance, p.Port))
 	}
 
 	workstation, ok := params["workstation id"]
@@ -293,7 +297,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 	if ok {
 		if appintent == "ReadOnly" {
 			if p.Database == "" {
-				return p, params, fmt.Errorf("database must be specified when ApplicationIntent is ReadOnly")
+				return p, fmt.Errorf("database must be specified when ApplicationIntent is ReadOnly")
 			}
 			p.ReadOnlyIntent = true
 		}
@@ -310,7 +314,7 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		p.FailOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
 		if err != nil {
 			f := "invalid failover port '%v': %v"
-			return p, params, fmt.Errorf(f, failOverPort, err.Error())
+			return p, fmt.Errorf(f, failOverPort, err.Error())
 		}
 	}
 
@@ -320,13 +324,13 @@ func Parse(dsn string) (Config, map[string]string, error) {
 		p.DisableRetry, err = strconv.ParseBool(disableRetry)
 		if err != nil {
 			f := "invalid disableRetry '%s': %s"
-			return p, params, fmt.Errorf(f, disableRetry, err.Error())
+			return p, fmt.Errorf(f, disableRetry, err.Error())
 		}
 	} else {
 		p.DisableRetry = disableRetryDefault
 	}
 
-	return p, params, nil
+	return p, nil
 }
 
 // convert connectionParams to url style connection string
@@ -608,6 +612,26 @@ func normalizeOdbcKey(s string) string {
 	return strings.ToLower(strings.TrimRightFunc(s, unicode.IsSpace))
 }
 
-func generateSpn(host string, port uint64) string {
-	return fmt.Sprintf("MSSQLSvc/%s:%d", host, port)
+func instanceOrPort(instance string, port uint64) string {
+	if len(instance) > 0 {
+		return instance
+	}
+
+	port = resolveServerPort(port)
+
+	return strconv.FormatInt(int64(port), 10)
+}
+
+const defaultServerPort = 1433
+
+func resolveServerPort(port uint64) uint64 {
+	if port == 0 {
+		return defaultServerPort
+	}
+
+	return port
+}
+
+func generateSpn(host string, port string) string {
+	return fmt.Sprintf("MSSQLSvc/%s:%s", host, port)
 }
diff --git a/mssql.go b/mssql.go
index f0cba5a..d036129 100644
--- a/mssql.go
+++ b/mssql.go
@@ -61,7 +61,7 @@ type Driver struct {
 
 // OpenConnector opens a new connector. Useful to dial with a context.
 func (d *Driver) OpenConnector(dsn string) (*Connector, error) {
-	params, _, err := msdsn.Parse(dsn)
+	params, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
@@ -115,7 +115,7 @@ func (d *Driver) SetContextLogger(ctxLogger ContextLogger) {
 // NewConnector creates a new connector from a DSN.
 // The returned connector may be used with sql.OpenDB.
 func NewConnector(dsn string) (*Connector, error) {
-	params, _, err := msdsn.Parse(dsn)
+	params, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
@@ -129,7 +129,7 @@ func NewConnector(dsn string) (*Connector, error) {
 // NewConnectorWithAccessTokenProvider creates a new connector from a DSN using the given
 // access token provider. The returned connector may be used with sql.OpenDB.
 func NewConnectorWithAccessTokenProvider(dsn string, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) {
-	params, _, err := msdsn.Parse(dsn)
+	params, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
@@ -387,7 +387,7 @@ func (c *Conn) processBeginResponse(ctx context.Context) (driver.Tx, error) {
 }
 
 func (d *Driver) open(ctx context.Context, dsn string) (*Conn, error) {
-	params, _, err := msdsn.Parse(dsn)
+	params, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
diff --git a/tds.go b/tds.go
index ced4a7d..82da5f0 100644
--- a/tds.go
+++ b/tds.go
@@ -16,6 +16,7 @@ import (
 	"unicode/utf16"
 	"unicode/utf8"
 
+	"github.com/microsoft/go-mssqldb/integratedauth"
 	"github.com/microsoft/go-mssqldb/msdsn"
 )
 
@@ -836,12 +837,6 @@ func sendAttention(buf *tdsBuffer) error {
 	return buf.FinishPacket()
 }
 
-type auth interface {
-	InitialBytes() ([]byte, error)
-	NextBytes([]byte) ([]byte, error)
-	Free()
-}
-
 // SQL Server AlwaysOn Availability Group Listeners are bound by DNS to a
 // list of IP addresses.  So if there is more than one, try them all and
 // use the first one that allows a connection.
@@ -966,7 +961,7 @@ func interpretPreloginResponse(p msdsn.Config, fe *featureExtFedAuth, fields map
 	return
 }
 
-func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger ContextLogger, auth auth, fe *featureExtFedAuth, packetSize uint32) (l *login, err error) {
+func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger ContextLogger, auth integratedauth.IntegratedAuthenticator, fe *featureExtFedAuth, packetSize uint32) (l *login, err error) {
 	var typeFlags uint8
 	if p.ReadOnlyIntent {
 		typeFlags |= fReadOnlyIntent
@@ -1171,11 +1166,17 @@ initiate_connection:
 		}
 	}
 
-	auth, authOk := getAuth(p.User, p.Password, p.ServerSPN, p.Workstation)
-	if authOk {
+	auth, err := integratedauth.GetIntegratedAuthenticator(p)
+	if err != nil {
+		if uint64(p.LogFlags)&logDebug != 0 {
+			logger.Log(ctx, msdsn.LogDebug, fmt.Sprintf("Error while creating integrated authenticator: %v", err))
+		}
+
+		return nil, err
+	}
+
+	if auth != nil {
 		defer auth.Free()
-	} else {
-		auth = nil
 	}
 
 	login, err := prepareLogin(ctx, c, p, logger, auth, fedAuth, uint32(outbuf.PackageSize()))
diff --git a/ucs22str.go b/ucs22str.go
index 80319d2..9c9c66e 100644
--- a/ucs22str.go
+++ b/ucs22str.go
@@ -1,5 +1,5 @@
-//go:build !386
-// +build !386
+//go:build !386 && !arm
+// +build !386,!arm
 
 package mssql
 
diff --git a/ucs22str_386.go b/ucs22str_32bit.go
similarity index 89%
rename from ucs22str_386.go
rename to ucs22str_32bit.go
index dccc3a6..22ef73b 100644
--- a/ucs22str_386.go
+++ b/ucs22str_32bit.go
@@ -1,3 +1,6 @@
+//go:build arm || 386
+// +build arm 386
+
 package mssql
 
 import (


Test patch:
diff --git a/azuread/configuration_test.go b/azuread/configuration_test.go
index 12f3282..e830d17 100644
--- a/azuread/configuration_test.go
+++ b/azuread/configuration_test.go
@@ -1,128 +1,128 @@
-//go:build go1.18
-// +build go1.18
-
-package azuread
-
-import (
-	"testing"
-
-	mssql "github.com/microsoft/go-mssqldb"
-	"github.com/microsoft/go-mssqldb/msdsn"
-)
-
-func TestValidateParameters(t *testing.T) {
-	passphrase := "somesecret"
-	certificatepath := "/user/cert/cert.pfx"
-	appid := "applicationclientid=someguid"
-	certprop := "clientcertpath=" + certificatepath
-	tests := []struct {
-		name     string
-		dsn      string
-		expected *azureFedAuthConfig
-	}{
-		{
-			name:     "no fed auth configured",
-			dsn:      "server=someserver",
-			expected: &azureFedAuthConfig{fedAuthLibrary: mssql.FedAuthLibraryReserved},
-		},
-		{
-			name: "application with cert/key",
-			dsn:  `sqlserver://service-principal-id%40tenant-id:somesecret@someserver.database.windows.net?fedauth=ActiveDirectoryApplication&` + certprop + "&" + appid,
-			expected: &azureFedAuthConfig{
-				fedAuthLibrary:      mssql.FedAuthLibraryADAL,
-				clientID:            "service-principal-id",
-				tenantID:            "tenant-id",
-				certificatePath:     certificatepath,
-				clientSecret:        passphrase,
-				adalWorkflow:        mssql.FedAuthADALWorkflowPassword,
-				fedAuthWorkflow:     ActiveDirectoryApplication,
-				applicationClientID: "someguid",
-			},
-		},
-		{
-			name: "application with cert/key missing tenant id",
-			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryApplication;user id=service-principal-id;password=somesecret;" + certprop + ";" + appid,
-			expected: &azureFedAuthConfig{
-				fedAuthLibrary:      mssql.FedAuthLibraryADAL,
-				clientID:            "service-principal-id",
-				certificatePath:     certificatepath,
-				clientSecret:        passphrase,
-				adalWorkflow:        mssql.FedAuthADALWorkflowPassword,
-				fedAuthWorkflow:     ActiveDirectoryApplication,
-				applicationClientID: "someguid",
-			},
-		},
-		{
-			name: "application with secret",
-			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryServicePrincipal;user id=service-principal-id@tenant-id;password=somesecret;",
-			expected: &azureFedAuthConfig{
-				clientID:        "service-principal-id",
-				tenantID:        "tenant-id",
-				clientSecret:    passphrase,
-				adalWorkflow:    mssql.FedAuthADALWorkflowPassword,
-				fedAuthWorkflow: ActiveDirectoryServicePrincipal,
-			},
-		},
-		{
-			name: "user with password",
-			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryPassword;user id=azure-ad-user@example.com;password=somesecret;" + appid,
-			expected: &azureFedAuthConfig{
-				adalWorkflow:        mssql.FedAuthADALWorkflowPassword,
-				user:                "azure-ad-user@example.com",
-				password:            passphrase,
-				applicationClientID: "someguid",
-				fedAuthWorkflow:     ActiveDirectoryPassword,
-			},
-		},
-		{
-			name: "managed identity without client id",
-			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryMSI",
-			expected: &azureFedAuthConfig{
-				adalWorkflow:    mssql.FedAuthADALWorkflowMSI,
-				fedAuthWorkflow: ActiveDirectoryMSI,
-			},
-		},
-		{
-			name: "managed identity with client id",
-			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;user id=identity-client-id",
-			expected: &azureFedAuthConfig{
-				adalWorkflow:    mssql.FedAuthADALWorkflowMSI,
-				clientID:        "identity-client-id",
-				fedAuthWorkflow: ActiveDirectoryManagedIdentity,
-			},
-		},
-		{
-			name: "managed identity with resource id",
-			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;resource id=/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
-			expected: &azureFedAuthConfig{
-				adalWorkflow:    mssql.FedAuthADALWorkflowMSI,
-				resourceID:      "/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
-				fedAuthWorkflow: ActiveDirectoryManagedIdentity,
-			},
-		},
-	}
-	for _, tst := range tests {
-		config, err := parse(tst.dsn)
-		if tst.expected == nil {
-			if err == nil {
-				t.Errorf("No error returned when error expected in test case '%s'", tst.name)
-			}
-			continue
-		}
-		if err != nil {
-			t.Errorf("Error returned when none expected in test case '%s': %v", tst.name, err)
-			continue
-		}
-		if tst.expected.fedAuthLibrary != mssql.FedAuthLibraryReserved {
-			if tst.expected.fedAuthLibrary == 0 {
-				tst.expected.fedAuthLibrary = mssql.FedAuthLibraryADAL
-			}
-		}
-		// mssqlConfig is not idempotent due to pointers in it, plus we aren't testing its correctness here
-		config.mssqlConfig = msdsn.Config{}
-		if *config != *tst.expected {
-			t.Errorf("Captured parameters do not match in test case '%s'. Expected:%+v, Actual:%+v", tst.name, tst.expected, config)
-		}
-	}
-
-}
+//go:build go1.18
+// +build go1.18
+
+package azuread
+
+import (
+	"reflect"
+	"testing"
+
+	mssql "github.com/microsoft/go-mssqldb"
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+func TestValidateParameters(t *testing.T) {
+	passphrase := "somesecret"
+	certificatepath := "/user/cert/cert.pfx"
+	appid := "applicationclientid=someguid"
+	certprop := "clientcertpath=" + certificatepath
+	tests := []struct {
+		name     string
+		dsn      string
+		expected *azureFedAuthConfig
+	}{
+		{
+			name:     "no fed auth configured",
+			dsn:      "server=someserver",
+			expected: &azureFedAuthConfig{fedAuthLibrary: mssql.FedAuthLibraryReserved},
+		},
+		{
+			name: "application with cert/key",
+			dsn:  `sqlserver://service-principal-id%40tenant-id:somesecret@someserver.database.windows.net?fedauth=ActiveDirectoryApplication&` + certprop + "&" + appid,
+			expected: &azureFedAuthConfig{
+				fedAuthLibrary:      mssql.FedAuthLibraryADAL,
+				clientID:            "service-principal-id",
+				tenantID:            "tenant-id",
+				certificatePath:     certificatepath,
+				clientSecret:        passphrase,
+				adalWorkflow:        mssql.FedAuthADALWorkflowPassword,
+				fedAuthWorkflow:     ActiveDirectoryApplication,
+				applicationClientID: "someguid",
+			},
+		},
+		{
+			name: "application with cert/key missing tenant id",
+			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryApplication;user id=service-principal-id;password=somesecret;" + certprop + ";" + appid,
+			expected: &azureFedAuthConfig{
+				fedAuthLibrary:      mssql.FedAuthLibraryADAL,
+				clientID:            "service-principal-id",
+				certificatePath:     certificatepath,
+				clientSecret:        passphrase,
+				adalWorkflow:        mssql.FedAuthADALWorkflowPassword,
+				fedAuthWorkflow:     ActiveDirectoryApplication,
+				applicationClientID: "someguid",
+			},
+		},
+		{
+			name: "application with secret",
+			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryServicePrincipal;user id=service-principal-id@tenant-id;password=somesecret;",
+			expected: &azureFedAuthConfig{
+				clientID:        "service-principal-id",
+				tenantID:        "tenant-id",
+				clientSecret:    passphrase,
+				adalWorkflow:    mssql.FedAuthADALWorkflowPassword,
+				fedAuthWorkflow: ActiveDirectoryServicePrincipal,
+			},
+		},
+		{
+			name: "user with password",
+			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryPassword;user id=azure-ad-user@example.com;password=somesecret;" + appid,
+			expected: &azureFedAuthConfig{
+				adalWorkflow:        mssql.FedAuthADALWorkflowPassword,
+				user:                "azure-ad-user@example.com",
+				password:            passphrase,
+				applicationClientID: "someguid",
+				fedAuthWorkflow:     ActiveDirectoryPassword,
+			},
+		},
+		{
+			name: "managed identity without client id",
+			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryMSI",
+			expected: &azureFedAuthConfig{
+				adalWorkflow:    mssql.FedAuthADALWorkflowMSI,
+				fedAuthWorkflow: ActiveDirectoryMSI,
+			},
+		},
+		{
+			name: "managed identity with client id",
+			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;user id=identity-client-id",
+			expected: &azureFedAuthConfig{
+				adalWorkflow:    mssql.FedAuthADALWorkflowMSI,
+				clientID:        "identity-client-id",
+				fedAuthWorkflow: ActiveDirectoryManagedIdentity,
+			},
+		},
+		{
+			name: "managed identity with resource id",
+			dsn:  "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;resource id=/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
+			expected: &azureFedAuthConfig{
+				adalWorkflow:    mssql.FedAuthADALWorkflowMSI,
+				resourceID:      "/subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}",
+				fedAuthWorkflow: ActiveDirectoryManagedIdentity,
+			},
+		},
+	}
+	for _, tst := range tests {
+		config, err := parse(tst.dsn)
+		if tst.expected == nil {
+			if err == nil {
+				t.Errorf("No error returned when error expected in test case '%s'", tst.name)
+			}
+			continue
+		}
+		if err != nil {
+			t.Errorf("Error returned when none expected in test case '%s': %v", tst.name, err)
+			continue
+		}
+		if tst.expected.fedAuthLibrary != mssql.FedAuthLibraryReserved {
+			if tst.expected.fedAuthLibrary == 0 {
+				tst.expected.fedAuthLibrary = mssql.FedAuthLibraryADAL
+			}
+		}
+		// mssqlConfig is not idempotent due to pointers in it, plus we aren't testing its correctness here
+		config.mssqlConfig = msdsn.Config{}
+		if !reflect.DeepEqual(config, tst.expected) {
+			t.Errorf("Captured parameters do not match in test case '%s'. Expected:%+v, Actual:%+v", tst.name, tst.expected, config)
+		}
+	}
+}
diff --git a/integratedauth/auth_test.go b/integratedauth/auth_test.go
new file mode 100644
index 0000000..a3d25e3
--- /dev/null
+++ b/integratedauth/auth_test.go
@@ -0,0 +1,203 @@
+package integratedauth
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/microsoft/go-mssqldb/msdsn"
+)
+
+const providerName = "stub"
+
+type stubAuth struct {
+	user string
+}
+
+func (s *stubAuth) InitialBytes() ([]byte, error)    { return nil, nil }
+func (s *stubAuth) NextBytes([]byte) ([]byte, error) { return nil, nil }
+func (s *stubAuth) Free()                            {}
+
+func getAuth(config msdsn.Config) (IntegratedAuthenticator, error) {
+	return &stubAuth{config.User}, nil
+}
+
+func TestSetIntegratedAuthenticationProviderReturnsErrOnNilProvider(t *testing.T) {
+	err := SetIntegratedAuthenticationProvider(providerName, nil)
+
+	if err != ErrProviderCannotBeNil {
+		t.Errorf("SetIntegratedAuthenticationProvider() returned err: %v, want %v", err, ErrProviderCannotBeNil)
+	}
+}
+
+func TestSetIntegratedAuthenticationProviderReturnsErrOnEmptyProviderName(t *testing.T) {
+	err := SetIntegratedAuthenticationProvider("", ProviderFunc(getAuth))
+
+	if err != ErrProviderNameMustBePopulated {
+		t.Errorf("SetIntegratedAuthenticationProvider() returned err: %v, want %v", err, ErrProviderNameMustBePopulated)
+	}
+}
+
+func TestSetIntegratedAuthenticationProviderStored(t *testing.T) {
+	err := SetIntegratedAuthenticationProvider(providerName, ProviderFunc(getAuth))
+	if err != nil {
+		t.Errorf("SetIntegratedAuthenticationProvider() returned unexpected err %v", err)
+	}
+	defer removeStubProvider()
+
+	if _, ok := providers[providerName]; !ok {
+		t.Error("SetIntegratedAuthenticationProvider() added provider not found")
+	}
+}
+
+func TestSetIntegratedAuthenticationProviderInstanceIsPassedConnString(t *testing.T) {
+	err := SetIntegratedAuthenticationProvider(providerName, ProviderFunc(getAuth))
+	if err != nil {
+		t.Errorf("SetIntegratedAuthenticationProvider() returned unexpected err %v", err)
+	}
+	defer removeStubProvider()
+
+	config, err := msdsn.Parse(fmt.Sprintf("authenticator=%v;user id=username", providerName))
+	if err != nil {
+		t.Errorf("msdsn.Parse : Unexpected error %v", err)
+		return
+	}
+
+	authenticator, err := GetIntegratedAuthenticator(config)
+
+	if err != nil {
+		t.Errorf("expected GetIntegratedAuthenticator() to return ok, found %v", err)
+	}
+
+	a, ok := authenticator.(*stubAuth)
+	if !ok {
+		t.Errorf("expected result of GetIntegratedAuthenticator() to be an instance of stubAuth")
+	}
+
+	if a.user != "username" {
+		t.Errorf("expected stubAuth username to be correct")
+	}
+}
+
+func TestSetIntegratedAuthenticationProviderInstanceIsDefaultWhenAuthenticatorParamNotPassed(t *testing.T) {
+	removeStubProvider()
+
+	config, err := msdsn.Parse("user id=username")
+	if err != nil {
+		t.Errorf("msdsn.Parse : Unexpected error %v", err)
+		return
+	}
+
+	DefaultProviderName = "DEFAULT_PROVIDER"
+	defer func() { DefaultProviderName = "" }()
+
+	err = SetIntegratedAuthenticationProvider(DefaultProviderName, ProviderFunc(func(config msdsn.Config) (IntegratedAuthenticator, error) {
+		return &stubAuth{"DEFAULT INSTANCE"}, nil
+	}))
+	if err != nil {
+		t.Errorf("SetIntegratedAuthenticationProvider() returned unexpected err %v", err)
+	}
+
+	result, err := GetIntegratedAuthenticator(config)
+
+	if err != nil {
+		t.Errorf("expected GetIntegratedAuthenticator() to return ok, found %v", err)
+	}
+
+	a, ok := result.(*stubAuth)
+	if !ok {
+		t.Errorf("expected result of GetIntegratedAuthenticator() to be an instance of stubAuth")
+	}
+
+	if a.user != "DEFAULT INSTANCE" {
+		t.Errorf("expected GetIntegratedAuthenticator for return DefaultProviderName instance when no authenticator param is passed, found %v", a.user)
+	}
+}
+
+func TestGetIntegratedAuthenticatorFallBackToSqlAuthOnErrorOfDefaultProvider(t *testing.T) {
+	removeStubProvider()
+
+	config, err := msdsn.Parse("user id=username")
+	if err != nil {
+		t.Errorf("msdsn.Parse : Unexpected error %v", err)
+		return
+	}
+
+	DefaultProviderName = "DEFAULT_PROVIDER"
+	defer func() { DefaultProviderName = "" }()
+
+	err = SetIntegratedAuthenticationProvider(DefaultProviderName, ProviderFunc(func(config msdsn.Config) (IntegratedAuthenticator, error) {
+		return nil, errors.New("default authenticator cant continue")
+	}))
+	if err != nil {
+		t.Errorf("SetIntegratedAuthenticationProvider() returned unexpected err %v", err)
+	}
+
+	result, err := GetIntegratedAuthenticator(config)
+
+	if err != nil {
+		t.Errorf("expected GetIntegratedAuthenticator() to return ok, found %v", err)
+	}
+
+	if result != nil {
+		t.Errorf("expected GetIntegratedAuthenticator() to return nill authenticator, found %v", result)
+	}
+}
+
+func TestGetIntegratedAuthenticatorToErrorWhenNoDefaultProviderFound(t *testing.T) {
+	removeStubProvider()
+
+	// dont set an authenticator
+	config, err := msdsn.Parse("user id=username")
+	if err != nil {
+		t.Errorf("msdsn.Parse : Unexpected error %v", err)
+		return
+	}
+
+	DefaultProviderName = "NONEXISTANT_DEFAULT_PROVIDER"
+	defer func() { DefaultProviderName = "" }()
+
+	result, err := GetIntegratedAuthenticator(config)
+
+	if err == nil {
+		t.Error("expected GetIntegratedAuthenticator() to return error, found nil")
+	}
+
+	if result != nil {
+		t.Errorf("expected GetIntegratedAuthenticator() to return nill provider, found %v", result)
+	}
+
+	if err != nil && err.Error() != "provider NONEXISTANT_DEFAULT_PROVIDER not found" {
+		t.Errorf("expected err that default provider was not found, found %v", err)
+	}
+}
+
+func TestGetIntegratedAuthenticatorToErrorWhenNoSpecifiedProviderFound(t *testing.T) {
+	removeStubProvider()
+	defer removeStubProvider()
+
+	config, err := msdsn.Parse("authenticator=NONEXISTANTPROVIDER;user id=username")
+	if err != nil {
+		t.Errorf("msdsn.Parse : Unexpected error %v", err)
+		return
+	}
+
+	// dont set an authenticator
+	result, err := GetIntegratedAuthenticator(config)
+
+	if err == nil {
+		t.Error("expected GetIntegratedAuthenticator() to return error, found nil")
+	}
+
+	if result != nil {
+		t.Errorf("expected GetIntegratedAuthenticator() to return nill provider, found %v", result)
+	}
+
+	if err != nil && err.Error() != "provider NONEXISTANTPROVIDER not found" {
+		t.Errorf("expected err that default provider was not found, found %v", err)
+	}
+}
+
+func removeStubProvider() {
+	delete(providers, providerName)
+}
diff --git a/ntlm_test.go b/integratedauth/ntlm/ntlm_test.go
similarity index 99%
rename from ntlm_test.go
rename to integratedauth/ntlm/ntlm_test.go
index edac77a..1df99e2 100644
--- a/ntlm_test.go
+++ b/integratedauth/ntlm/ntlm_test.go
@@ -1,6 +1,4 @@
-// +build !windows
-
-package mssql
+package ntlm
 
 import (
 	"bytes"
diff --git a/msdsn/conn_str_test.go b/msdsn/conn_str_test.go
index a760187..20a6cd2 100644
--- a/msdsn/conn_str_test.go
+++ b/msdsn/conn_str_test.go
@@ -35,7 +35,7 @@ func TestInvalidConnectionString(t *testing.T) {
 		"sqlserver://host?key=value1&key=value2", // duplicate keys
 	}
 	for _, connStr := range connStrings {
-		_, _, err := Parse(connStr)
+		_, err := Parse(connStr)
 		if err == nil {
 			t.Errorf("Connection expected to fail for connection string %s but it didn't", connStr)
 			continue
@@ -181,7 +181,7 @@ func TestValidConnectionString(t *testing.T) {
 		}},
 	}
 	for _, ts := range connStrings {
-		p, _, err := Parse(ts.connStr)
+		p, err := Parse(ts.connStr)
 		if err == nil {
 			t.Logf("Connection string was parsed successfully %s", ts.connStr)
 		} else {
@@ -203,12 +203,12 @@ func TestSplitConnectionStringURL(t *testing.T) {
 }
 
 func TestConnParseRoundTripFixed(t *testing.T) {
-	connStr := "sqlserver://sa:sa@localhost/sqlexpress?database=master&log=127"
-	params, _, err := Parse(connStr)
+	connStr := "sqlserver://sa:sa@localhost/sqlexpress?database=master&log=127&disableretry=true"
+	params, err := Parse(connStr)
 	if err != nil {
 		t.Fatal("Test URL is not valid", err)
 	}
-	rtParams, _, err := Parse(params.URL().String())
+	rtParams, err := Parse(params.URL().String())
 	if err != nil {
 		t.Fatal("Params after roundtrip are not valid", err)
 	}
@@ -216,3 +216,39 @@ func TestConnParseRoundTripFixed(t *testing.T) {
 		t.Fatal("Parameters do not match after roundtrip", params, rtParams)
 	}
 }
+
+func TestAllKeysAreAvailableInParametersMap(t *testing.T) {
+	keys := map[string]string{
+		"user id":            "1",
+		"testparam":          "testvalue",
+		"password":           "test",
+		"thisisanunknownkey": "thisisthevalue",
+		"server":             "name",
+	}
+
+	connString := ""
+	for key, val := range keys {
+		connString += key + "=" + val + ";"
+	}
+
+	params, err := Parse(connString)
+	if err != nil {
+		t.Errorf("unexpected error while parsing, %v", err)
+	}
+
+	if params.Parameters == nil {
+		t.Error("Expected parameters map to be instanciated, found nil")
+		return
+	}
+
+	if len(params.Parameters) != len(keys) {
+		t.Errorf("Expected parameters map to be same length as input map length, expected %v, found %v", len(keys), len(params.Parameters))
+		return
+	}
+
+	for key, val := range keys {
+		if params.Parameters[key] != val {
+			t.Errorf("Expected parameters map to contain key %v and value %v, found %v", key, val, params.Parameters[key])
+		}
+	}
+}
diff --git a/tds_login_test.go b/tds_login_test.go
index 3bb11e5..e6d1beb 100644
--- a/tds_login_test.go
+++ b/tds_login_test.go
@@ -170,7 +170,7 @@ func TestLoginWithSQLServerAuth(t *testing.T) {
 }
 
 func TestLoginWithSecurityTokenAuth(t *testing.T) {
-	config, _, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -231,7 +231,7 @@ func TestLoginWithSecurityTokenAuth(t *testing.T) {
 }
 
 func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
-	config, _, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -305,7 +305,7 @@ func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
 }
 
 func TestLoginWithADALManagedIdentityAuth(t *testing.T) {
-	config, _, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	config, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/tds_test.go b/tds_test.go
index 0caeed8..0dfeeb2 100644
--- a/tds_test.go
+++ b/tds_test.go
@@ -156,7 +156,7 @@ func TestSendLoginWithFeatureExt(t *testing.T) {
 
 func TestSendSqlBatch(t *testing.T) {
 	checkConnStr(t)
-	p, _, err := msdsn.Parse(makeConnStr(t).String())
+	p, err := msdsn.Parse(makeConnStr(t).String())
 	if err != nil {
 		t.Error("parseConnectParams failed:", err.Error())
 		return
@@ -225,7 +225,7 @@ func GetConnParams() (*msdsn.Config, error) {
 	dsn := os.Getenv("SQLSERVER_DSN")
 	const logFlags = 127
 	if len(dsn) > 0 {
-		params, _, err := msdsn.Parse(dsn)
+		params, err := msdsn.Parse(dsn)
 		if err != nil {
 			return nil, err
 		}
@@ -250,7 +250,7 @@ func GetConnParams() (*msdsn.Config, error) {
 		if err != io.EOF && err != nil {
 			return nil, err
 		}
-		params, _, err := msdsn.Parse(dsn)
+		params, err := msdsn.Parse(dsn)
 		if err != nil {
 			return nil, err
 		}
@@ -875,7 +875,7 @@ func TestReadBVarByte(t *testing.T) {
 
 func BenchmarkPacketSize(b *testing.B) {
 	checkConnStr(b)
-	p, _, err := msdsn.Parse(makeConnStr(b).String())
+	p, err := msdsn.Parse(makeConnStr(b).String())
 	if err != nil {
 		b.Error("parseConnectParams failed:", err.Error())
 		return