Skip to content

Commit f6fd477

Browse files
authoredJan 11, 2022
Add AAD auth using azidentity (denisenkom#698)
azuread: add azuread package to implement AAD auth
1 parent 5ec1570 commit f6fd477

14 files changed

+539
-55
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ coverage.json
1010
coverage.txt
1111
coverage.xml
1212
testresults.xml
13+
.azureconnstr
1314

‎.pipelines/TestSql2017.yml

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pool:
22
vmImage: 'ubuntu-latest'
33

44
trigger: none
5+
variables:
6+
TESTPASSWORD: $(SQLPASSWORD)
57

68
steps:
79
- task: GoTool@0
@@ -39,24 +41,26 @@ steps:
3941
arguments: 'github.com/AlekSi/gocov-xml@latest'
4042
workingDirectory: '$(System.DefaultWorkingDirectory)'
4143

42-
#Your build pipeline references an undefined variables named SQLPASSWORD and HOST.
44+
#Your build pipeline references an undefined variables named SQLPASSWORD and AZURESERVER_DSN.
4345
#Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972
4446

4547
- task: Docker@2
4648
displayName: 'Run SQL 2017 docker image'
4749
inputs:
4850
command: run
49-
arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(SQLPASSWORD) mcr.microsoft.com/mssql/server:2017-latest'
51+
arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(TESTPASSWORD) mcr.microsoft.com/mssql/server:2017-latest'
5052

5153
- script: |
52-
~/go/bin/gotestsum --junitfile testresults.xml -- -coverprofile=coverage.txt -covermode count
54+
~/go/bin/gotestsum --junitfile testresults.xml -- -coverprofile=coverage.txt -covermode count ./...
5355
~/go/bin/gocov convert coverage.txt > coverage.json
5456
~/go/bin/gocov-xml < coverage.json > coverage.xml
5557
mkdir coverage
5658
workingDirectory: '$(Build.SourcesDirectory)'
5759
displayName: 'run tests'
5860
env:
59-
SQLPASSWORD: $(SQLPASSWORD)
61+
SQLSERVER_DSN: 'server=.;user id=sa;password=$(TESTPASSWORD)'
62+
AZURESERVER_DSN: $(AZURESERVER_DSN)
63+
6064
continueOnError: true
6165
- task: PublishTestResults@2
6266
displayName: "Publish junit-style results"

‎README.md

+31-16
Original file line numberDiff line numberDiff line change
@@ -112,30 +112,43 @@ Other supported formats are listed below.
112112
* `odbc:server=localhost;user id=sa;password={foo{bar}` // Literal `{`, password is "foo{bar"
113113
* `odbc:server=localhost;user id=sa;password={foo}}bar}` // Escaped `} with`}}`, password is "foo}bar"
114114

115-
### Azure Active Directory authentication - preview
115+
### Azure Active Directory authentication
116+
117+
Azure Active Directory authentication uses temporary authentication tokens to authenticate.
118+
The `mssql` package does not provide an implementation to obtain tokens: instead, import the `azuread` package and use driver name `azuresql`. This driver uses [azidentity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#section-readme) to acquire tokens using a variety of credential types.
119+
120+
The credential type is determined by the new `fedauth` connection string parameter.
121+
122+
* `fedauth=ActiveDirectoryServicePrincipal` or `fedauth=ActiveDirectoryApplication` - authenticates using an Azure Active Directory application client ID and client secret or certificate. Implemented using [ClientSecretCredential or CertificateCredential](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity#authenticating-service-principals)
123+
* `clientcertpath=<path to certificate file>;password=<certificate password>` or
124+
* `password=<client secret>`
125+
* `user id=<application id>[@tenantid]` Note the `@tenantid` component can be omitted if the server's tenant is the same as the application's tenant.
126+
* `fedauth=ActiveDirectoryPassword` - authenticates using a user name and password.
127+
* `user id=username@domain`
128+
* `password=<password>`
129+
* `applicationclientid=<application id>` - This guid identifies an Azure Active Directory enterprise application that the AAD admin has approved for accessing Azure SQL database resources in the tenant. This driver does not have an associated application id of its own.
130+
* `fedauth=ActiveDirectoryDefault` - authenticates using a chained set of credentials. The chain is built from EnvironmentCredential -> ManagedIdentityCredential->AzureCLICredential. See [DefaultAzureCredential docs](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential) for instructions on setting up your host environment to use it. Using this option allows you to have the same connection string in a service deployment as on your interactive development machine.
131+
* `fedauth=ActiveDirectoryManagedIdentity` or `fedauth=ActiveDirectoryMSI` - authenticates using a system-assigned or user-assigned Azure Managed Identity.
132+
* `user id=<identity id>` - optional id of user-assigned managed identity. If empty, system-assigned managed identity is used.
133+
* `fedauth=ActiveDirectoryInteractive` - authenticates using credentials acquired from an external web browser. Only suitable for use with human interaction.
134+
* `applicationclientid=<application id>` - This guid identifies an Azure Active Directory enterprise application that the AAD admin has approved for accessing Azure SQL database resources in the tenant. This driver does not have an associated application id of its own.
116135

117-
The configuration of functionality might change in the future.
136+
```go
118137
119-
Azure Active Directory (AAD) access tokens are relatively short lived and need to be
120-
valid when a new connection is made. Authentication is supported using a callback func that
121-
provides a fresh and valid token using a connector:
138+
import (
139+
"database/sql"
140+
"net/url"
122141
123-
``` go
142+
// Import the Azure AD driver module (also imports the regular driver package)
143+
"github.com/denisenkom/go-mssqldb/azuread"
144+
)
124145
125-
conn, err := mssql.NewAccessTokenConnector(
126-
"Server=test.database.windows.net;Database=testdb",
127-
tokenProvider)
128-
if err != nil {
129-
// handle errors in DSN
146+
func ConnectWithMSI() (*sql.DB, error) {
147+
return sql.Open(azuread.DriverName, "sqlserver://azuresql.database.windows.net?database=yourdb&fedauth=ActiveDirectoryMSI")
130148
}
131-
db := sql.OpenDB(conn)
132149
133150
```
134151

135-
Where `tokenProvider` is a function that returns a fresh access token or an error. None of these statements
136-
actually trigger the retrieval of a token, this happens when the first statment is issued and a connection
137-
is created.
138-
139152
## Executing Stored Procedures
140153

141154
To run a stored procedure, set the query text to the procedure name:
@@ -306,6 +319,8 @@ Example:
306319
env SQLSERVER_DSN=sqlserver://user:pass@hostname/instance?database=test1 go test
307320
```
308321

322+
`AZURESERVER_DSN` environment variable provides the connection string for Azure Active Directory-based authentication. If it's not set the AAD test will be skipped.
323+
309324
## Deprecated
310325
311326
These features still exist in the driver, but they are are deprecated.

‎accesstokenconnector.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func NewAccessTokenConnector(dsn string, tokenProvider func() (string, error)) (
2222
}
2323

2424
conn.fedAuthRequired = true
25-
conn.fedAuthLibrary = fedAuthLibrarySecurityToken
25+
conn.fedAuthLibrary = FedAuthLibrarySecurityToken
2626
conn.securityTokenProvider = func(ctx context.Context) (string, error) {
2727
return tokenProvider()
2828
}

‎azuread/azuread_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package azuread
2+
3+
import (
4+
"bufio"
5+
"database/sql"
6+
"io"
7+
"os"
8+
"testing"
9+
10+
mssql "github.com/denisenkom/go-mssqldb"
11+
)
12+
13+
func TestAzureSqlAuth(t *testing.T) {
14+
mssqlConfig := testConnParams(t)
15+
16+
conn, err := newConnectorConfig(mssqlConfig)
17+
if err != nil {
18+
t.Fatalf("Unable to get a connector: %v", err)
19+
}
20+
db := sql.OpenDB(conn)
21+
row := db.QueryRow("select 100, suser_sname()")
22+
var val int
23+
var user string
24+
err = row.Scan(&val, &user)
25+
if err != nil {
26+
t.Fatalf("Unable to query the db: %v", err)
27+
}
28+
if val != 100 {
29+
t.Fatalf("Got wrong value from query. Expected:100, Got: %d", val)
30+
}
31+
t.Logf("Got suser_sname value %s", user)
32+
33+
}
34+
35+
// returns parsed connection parameters derived from
36+
// environment variables
37+
func testConnParams(t testing.TB) *azureFedAuthConfig {
38+
dsn := os.Getenv("AZURESERVER_DSN")
39+
const logFlags = 127
40+
if dsn == "" {
41+
// try loading connection string from file
42+
f, err := os.Open(".azureconnstr")
43+
if err == nil {
44+
rdr := bufio.NewReader(f)
45+
dsn, err = rdr.ReadString('\n')
46+
if err != io.EOF && err != nil {
47+
t.Fatal(err)
48+
}
49+
}
50+
}
51+
if dsn == "" {
52+
t.Skip("no azure database connection string. set AZURESERVER_DSN environment variable or create .azureconnstr file")
53+
}
54+
config, err := parse(dsn)
55+
if err != nil {
56+
t.Skip("error parsing connection string ")
57+
}
58+
if config.fedAuthLibrary == mssql.FedAuthLibraryReserved {
59+
t.Skip("Skipping azure test due to missing fedauth parameter ")
60+
}
61+
config.mssqlConfig.LogFlags = logFlags
62+
return config
63+
}

‎azuread/configuration.go

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package azuread
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
10+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
11+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
12+
mssql "github.com/denisenkom/go-mssqldb"
13+
"github.com/denisenkom/go-mssqldb/msdsn"
14+
)
15+
16+
const (
17+
ActiveDirectoryDefault = "ActiveDirectoryDefault"
18+
ActiveDirectoryIntegrated = "ActiveDirectoryIntegrated"
19+
ActiveDirectoryPassword = "ActiveDirectoryPassword"
20+
ActiveDirectoryInteractive = "ActiveDirectoryInteractive"
21+
// ActiveDirectoryMSI is a synonym for ActiveDirectoryManagedIdentity
22+
ActiveDirectoryMSI = "ActiveDirectoryMSI"
23+
ActiveDirectoryManagedIdentity = "ActiveDirectoryManagedIdentity"
24+
// ActiveDirectoryApplication is a synonym for ActiveDirectoryServicePrincipal
25+
ActiveDirectoryApplication = "ActiveDirectoryApplication"
26+
ActiveDirectoryServicePrincipal = "ActiveDirectoryServicePrincipal"
27+
scopeDefaultSuffix = "/.default"
28+
)
29+
30+
type azureFedAuthConfig struct {
31+
adalWorkflow byte
32+
mssqlConfig msdsn.Config
33+
// The detected federated authentication library
34+
fedAuthLibrary int
35+
fedAuthWorkflow string
36+
// Service principal logins
37+
clientID string
38+
tenantID string
39+
clientSecret string
40+
certificatePath string
41+
42+
// AD password/managed identity/interactive
43+
user string
44+
password string
45+
applicationClientID string
46+
}
47+
48+
// parse returns a config based on an msdsn-style connection string
49+
func parse(dsn string) (*azureFedAuthConfig, error) {
50+
mssqlConfig, params, err := msdsn.Parse(dsn)
51+
if err != nil {
52+
return nil, err
53+
}
54+
config := &azureFedAuthConfig{
55+
fedAuthLibrary: mssql.FedAuthLibraryReserved,
56+
mssqlConfig: mssqlConfig,
57+
}
58+
59+
err = config.validateParameters(params)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return config, nil
65+
}
66+
67+
func (p *azureFedAuthConfig) validateParameters(params map[string]string) error {
68+
69+
fedAuthWorkflow, _ := params["fedauth"]
70+
if fedAuthWorkflow == "" {
71+
return nil
72+
}
73+
74+
p.fedAuthLibrary = mssql.FedAuthLibraryADAL
75+
76+
p.applicationClientID, _ = params["applicationclientid"]
77+
78+
switch {
79+
case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryPassword):
80+
if p.applicationClientID == "" {
81+
return errors.New("applicationclientid parameter is required for " + ActiveDirectoryPassword)
82+
}
83+
p.adalWorkflow = mssql.FedAuthADALWorkflowPassword
84+
p.user, _ = params["user id"]
85+
p.password, _ = params["password"]
86+
case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryIntegrated):
87+
// Active Directory Integrated authentication is not fully supported:
88+
// you can only use this by also implementing an a token provider
89+
// and supplying it via ActiveDirectoryTokenProvider in the Connection.
90+
p.adalWorkflow = mssql.FedAuthADALWorkflowIntegrated
91+
case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryManagedIdentity) || strings.EqualFold(fedAuthWorkflow, ActiveDirectoryMSI):
92+
// When using MSI, to request a specific client ID or user-assigned identity,
93+
// provide the ID in the "user id" parameter
94+
p.adalWorkflow = mssql.FedAuthADALWorkflowMSI
95+
p.clientID, _ = splitTenantAndClientID(params["user id"])
96+
case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryApplication) || strings.EqualFold(fedAuthWorkflow, ActiveDirectoryServicePrincipal):
97+
p.adalWorkflow = mssql.FedAuthADALWorkflowPassword
98+
// Split the clientID@tenantID format
99+
// If no tenant is provided we'll use the one from the server
100+
p.clientID, p.tenantID = splitTenantAndClientID(params["user id"])
101+
if p.clientID == "" {
102+
return errors.New("Must provide 'client id[@tenant id]' as username parameter when using ActiveDirectoryApplication authentication")
103+
}
104+
105+
p.clientSecret, _ = params["password"]
106+
107+
p.certificatePath, _ = params["clientcertpath"]
108+
109+
if p.certificatePath == "" && p.clientSecret == "" {
110+
return errors.New("Must provide 'password' parameter when using ActiveDirectoryApplication authentication without cert/key credentials")
111+
}
112+
case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryDefault):
113+
p.adalWorkflow = mssql.FedAuthADALWorkflowPassword
114+
case strings.EqualFold(fedAuthWorkflow, ActiveDirectoryInteractive):
115+
if p.applicationClientID == "" {
116+
return errors.New("applicationclientid parameter is required for " + ActiveDirectoryInteractive)
117+
}
118+
p.adalWorkflow = mssql.FedAuthADALWorkflowPassword
119+
// user is an optional login hint
120+
p.user, _ = params["user id"]
121+
// we don't really have a password but we need to use some value.
122+
p.adalWorkflow = mssql.FedAuthADALWorkflowPassword
123+
124+
default:
125+
return fmt.Errorf("Invalid federated authentication type '%s': expected one of %+v",
126+
fedAuthWorkflow,
127+
[]string{ActiveDirectoryApplication, ActiveDirectoryServicePrincipal, ActiveDirectoryDefault, ActiveDirectoryIntegrated, ActiveDirectoryInteractive, ActiveDirectoryManagedIdentity, ActiveDirectoryMSI, ActiveDirectoryPassword})
128+
}
129+
p.fedAuthWorkflow = fedAuthWorkflow
130+
return nil
131+
}
132+
133+
func splitTenantAndClientID(user string) (string, string) {
134+
// Split the user name into client id and tenant id at the @ symbol
135+
at := strings.IndexRune(user, '@')
136+
if at < 1 || at >= (len(user)-1) {
137+
return user, ""
138+
}
139+
140+
return user[0:at], user[at+1:]
141+
}
142+
143+
func splitAuthorityAndTenant(authorityUrl string) (string, string) {
144+
separatorIndex := strings.LastIndex(authorityUrl, "/")
145+
tenant := authorityUrl[separatorIndex+1:]
146+
authority := authorityUrl[:separatorIndex]
147+
return authority, tenant
148+
}
149+
150+
func (p *azureFedAuthConfig) provideActiveDirectoryToken(ctx context.Context, serverSPN, stsURL string) (string, error) {
151+
var cred azcore.TokenCredential
152+
var err error
153+
authority, tenant := splitAuthorityAndTenant(stsURL)
154+
// client secret connection strings may override the server tenant
155+
if p.tenantID != "" {
156+
tenant = p.tenantID
157+
}
158+
scope := stsURL
159+
if !strings.HasSuffix(serverSPN, scopeDefaultSuffix) {
160+
scope = strings.TrimRight(serverSPN, "/") + scopeDefaultSuffix
161+
}
162+
163+
switch p.fedAuthWorkflow {
164+
case ActiveDirectoryServicePrincipal, ActiveDirectoryApplication:
165+
switch {
166+
case p.certificatePath != "":
167+
cred, err = azidentity.NewClientCertificateCredential(tenant, p.clientID, p.certificatePath, &azidentity.ClientCertificateCredentialOptions{Password: p.clientSecret})
168+
default:
169+
cred, err = azidentity.NewClientSecretCredential(tenant, p.clientID, p.clientSecret, nil)
170+
}
171+
case ActiveDirectoryPassword:
172+
cred, err = azidentity.NewUsernamePasswordCredential(tenant, p.applicationClientID, p.user, p.password, nil)
173+
case ActiveDirectoryMSI, ActiveDirectoryManagedIdentity:
174+
cred, err = azidentity.NewManagedIdentityCredential(p.clientID, nil)
175+
case ActiveDirectoryInteractive:
176+
cred, err = azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{AuthorityHost: authority, ClientID: p.applicationClientID})
177+
178+
default:
179+
// Integrated just uses Default until azidentity adds Windows-specific authentication
180+
cred, err = azidentity.NewDefaultAzureCredential(nil)
181+
}
182+
183+
if err != nil {
184+
return "", err
185+
}
186+
opts := policy.TokenRequestOptions{Scopes: []string{scope}}
187+
tk, err := cred.GetToken(ctx, opts)
188+
if err != nil {
189+
return "", err
190+
}
191+
return tk.Token, err
192+
}

‎azuread/configuration_test.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package azuread
2+
3+
import (
4+
"testing"
5+
6+
mssql "github.com/denisenkom/go-mssqldb"
7+
"github.com/denisenkom/go-mssqldb/msdsn"
8+
)
9+
10+
func TestValidateParameters(t *testing.T) {
11+
passphrase := "somesecret"
12+
certificatepath := "/user/cert/cert.pfx"
13+
appid := "applicationclientid=someguid"
14+
certprop := "clientcertpath=" + certificatepath
15+
tests := []struct {
16+
name string
17+
dsn string
18+
expected *azureFedAuthConfig
19+
}{
20+
{
21+
name: "no fed auth configured",
22+
dsn: "server=someserver",
23+
expected: &azureFedAuthConfig{fedAuthLibrary: mssql.FedAuthLibraryReserved},
24+
},
25+
{
26+
name: "application with cert/key",
27+
dsn: `sqlserver://service-principal-id%40tenant-id:somesecret@someserver.database.windows.net?fedauth=ActiveDirectoryApplication&` + certprop + "&" + appid,
28+
expected: &azureFedAuthConfig{
29+
fedAuthLibrary: mssql.FedAuthLibraryADAL,
30+
clientID: "service-principal-id",
31+
tenantID: "tenant-id",
32+
certificatePath: certificatepath,
33+
clientSecret: passphrase,
34+
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
35+
fedAuthWorkflow: ActiveDirectoryApplication,
36+
applicationClientID: "someguid",
37+
},
38+
},
39+
{
40+
name: "application with cert/key missing tenant id",
41+
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryApplication;user id=service-principal-id;password=somesecret;" + certprop + ";" + appid,
42+
expected: &azureFedAuthConfig{
43+
fedAuthLibrary: mssql.FedAuthLibraryADAL,
44+
clientID: "service-principal-id",
45+
certificatePath: certificatepath,
46+
clientSecret: passphrase,
47+
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
48+
fedAuthWorkflow: ActiveDirectoryApplication,
49+
applicationClientID: "someguid",
50+
},
51+
},
52+
{
53+
name: "application with secret",
54+
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryServicePrincipal;user id=service-principal-id@tenant-id;password=somesecret;",
55+
expected: &azureFedAuthConfig{
56+
clientID: "service-principal-id",
57+
tenantID: "tenant-id",
58+
clientSecret: passphrase,
59+
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
60+
fedAuthWorkflow: ActiveDirectoryServicePrincipal,
61+
},
62+
},
63+
{
64+
name: "user with password",
65+
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryPassword;user id=azure-ad-user@example.com;password=somesecret;" + appid,
66+
expected: &azureFedAuthConfig{
67+
adalWorkflow: mssql.FedAuthADALWorkflowPassword,
68+
user: "azure-ad-user@example.com",
69+
password: passphrase,
70+
applicationClientID: "someguid",
71+
fedAuthWorkflow: ActiveDirectoryPassword,
72+
},
73+
},
74+
{
75+
name: "managed identity without client id",
76+
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryMSI",
77+
expected: &azureFedAuthConfig{
78+
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
79+
fedAuthWorkflow: ActiveDirectoryMSI,
80+
},
81+
},
82+
{
83+
name: "managed identity with client id",
84+
dsn: "server=someserver.database.windows.net;fedauth=ActiveDirectoryManagedIdentity;user id=identity-client-id",
85+
expected: &azureFedAuthConfig{
86+
adalWorkflow: mssql.FedAuthADALWorkflowMSI,
87+
clientID: "identity-client-id",
88+
fedAuthWorkflow: ActiveDirectoryManagedIdentity,
89+
},
90+
},
91+
}
92+
for _, tst := range tests {
93+
config, err := parse(tst.dsn)
94+
if tst.expected == nil {
95+
if err == nil {
96+
t.Errorf("No error returned when error expected in test case '%s'", tst.name)
97+
}
98+
continue
99+
}
100+
if err != nil {
101+
t.Errorf("Error returned when none expected in test case '%s': %v", tst.name, err)
102+
continue
103+
}
104+
if tst.expected.fedAuthLibrary != mssql.FedAuthLibraryReserved {
105+
if tst.expected.fedAuthLibrary == 0 {
106+
tst.expected.fedAuthLibrary = mssql.FedAuthLibraryADAL
107+
}
108+
}
109+
// mssqlConfig is not idempotent due to pointers in it, plus we aren't testing its correctness here
110+
config.mssqlConfig = msdsn.Config{}
111+
if *config != *tst.expected {
112+
t.Errorf("Captured parameters do not match in test case '%s'. Expected:%+v, Actual:%+v", tst.name, tst.expected, config)
113+
}
114+
}
115+
116+
}

‎azuread/driver.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package azuread
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"database/sql/driver"
7+
8+
mssql "github.com/denisenkom/go-mssqldb"
9+
)
10+
11+
// DriverName is the name used to register the driver
12+
const DriverName = "azuresql"
13+
14+
func init() {
15+
sql.Register(DriverName, &Driver{})
16+
}
17+
18+
// Driver wraps the underlying MSSQL driver, but configures the Azure AD token provider
19+
type Driver struct {
20+
}
21+
22+
// Open returns a new connection to the database.
23+
func (d *Driver) Open(dsn string) (driver.Conn, error) {
24+
c, err := NewConnector(dsn)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
return c.Connect(context.Background())
30+
}
31+
32+
// NewConnector creates a new connector from a DSN.
33+
// The returned connector may be used with sql.OpenDB.
34+
func NewConnector(dsn string) (*mssql.Connector, error) {
35+
36+
config, err := parse(dsn)
37+
if err != nil {
38+
return nil, err
39+
}
40+
return newConnectorConfig(config)
41+
}
42+
43+
// newConnectorConfig creates a Connector from config.
44+
func newConnectorConfig(config *azureFedAuthConfig) (*mssql.Connector, error) {
45+
if config.fedAuthLibrary == mssql.FedAuthLibraryADAL {
46+
return mssql.NewActiveDirectoryTokenConnector(
47+
config.mssqlConfig, config.adalWorkflow,
48+
func(ctx context.Context, serverSPN, stsURL string) (string, error) {
49+
return config.provideActiveDirectoryToken(ctx, serverSPN, stsURL)
50+
},
51+
)
52+
}
53+
return mssql.NewConnectorConfig(config.mssqlConfig), nil
54+
}

‎fedauth.go

+17-17
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,46 @@ import (
99

1010
// Federated authentication library affects the login data structure and message sequence.
1111
const (
12-
// fedAuthLibraryLiveIDCompactToken specifies the Microsoft Live ID Compact Token authentication scheme
13-
fedAuthLibraryLiveIDCompactToken = 0x00
12+
// FedAuthLibraryLiveIDCompactToken specifies the Microsoft Live ID Compact Token authentication scheme
13+
FedAuthLibraryLiveIDCompactToken = 0x00
1414

15-
// fedAuthLibrarySecurityToken specifies a token-based authentication where the token is available
15+
// FedAuthLibrarySecurityToken specifies a token-based authentication where the token is available
1616
// without additional information provided during the login sequence.
17-
fedAuthLibrarySecurityToken = 0x01
17+
FedAuthLibrarySecurityToken = 0x01
1818

19-
// fedAuthLibraryADAL specifies a token-based authentication where a token is obtained during the
19+
// FedAuthLibraryADAL specifies a token-based authentication where a token is obtained during the
2020
// login sequence using the server SPN and STS URL provided by the server during login.
21-
fedAuthLibraryADAL = 0x02
21+
FedAuthLibraryADAL = 0x02
2222

23-
// fedAuthLibraryReserved is used to indicate that no federated authentication scheme applies.
24-
fedAuthLibraryReserved = 0x7F
23+
// FedAuthLibraryReserved is used to indicate that no federated authentication scheme applies.
24+
FedAuthLibraryReserved = 0x7F
2525
)
2626

2727
// Federated authentication ADAL workflow affects the mechanism used to authenticate.
2828
const (
29-
// fedAuthADALWorkflowPassword uses a username/password to obtain a token from Active Directory
30-
fedAuthADALWorkflowPassword = 0x01
29+
// FedAuthADALWorkflowPassword uses a username/password to obtain a token from Active Directory
30+
FedAuthADALWorkflowPassword = 0x01
3131

3232
// fedAuthADALWorkflowPassword uses the Windows identity to obtain a token from Active Directory
33-
fedAuthADALWorkflowIntegrated = 0x02
33+
FedAuthADALWorkflowIntegrated = 0x02
3434

35-
// fedAuthADALWorkflowMSI uses the managed identity service to obtain a token
36-
fedAuthADALWorkflowMSI = 0x03
35+
// FedAuthADALWorkflowMSI uses the managed identity service to obtain a token
36+
FedAuthADALWorkflowMSI = 0x03
3737
)
3838

3939
// newSecurityTokenConnector creates a new connector from a Config and a token provider.
4040
// When invoked, token provider implementations should contact the security token
4141
// service specified and obtain the appropriate token, or return an error
4242
// to indicate why a token is not available.
4343
// The returned connector may be used with sql.OpenDB.
44-
func newSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) {
44+
func NewSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) {
4545
if tokenProvider == nil {
4646
return nil, errors.New("mssql: tokenProvider cannot be nil")
4747
}
4848

4949
conn := NewConnectorConfig(config)
5050
conn.fedAuthRequired = true
51-
conn.fedAuthLibrary = fedAuthLibrarySecurityToken
51+
conn.fedAuthLibrary = FedAuthLibrarySecurityToken
5252
conn.securityTokenProvider = tokenProvider
5353

5454
return conn, nil
@@ -63,14 +63,14 @@ func newSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx conte
6363
// to indicate why a token is not available.
6464
//
6565
// The returned connector may be used with sql.OpenDB.
66-
func newActiveDirectoryTokenConnector(config msdsn.Config, adalWorkflow byte, tokenProvider func(ctx context.Context, serverSPN, stsURL string) (string, error)) (*Connector, error) {
66+
func NewActiveDirectoryTokenConnector(config msdsn.Config, adalWorkflow byte, tokenProvider func(ctx context.Context, serverSPN, stsURL string) (string, error)) (*Connector, error) {
6767
if tokenProvider == nil {
6868
return nil, errors.New("mssql: tokenProvider cannot be nil")
6969
}
7070

7171
conn := NewConnectorConfig(config)
7272
conn.fedAuthRequired = true
73-
conn.fedAuthLibrary = fedAuthLibraryADAL
73+
conn.fedAuthLibrary = FedAuthLibraryADAL
7474
conn.fedAuthADALWorkflow = adalWorkflow
7575
conn.adalTokenProvider = tokenProvider
7676

‎go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module github.com/denisenkom/go-mssqldb
33
go 1.11
44

55
require (
6+
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0
7+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0
68
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe
7-
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c
9+
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
810
)

‎go.sum

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
1+
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 h1:lhSJz9RMbJcTgxifR1hUNJnn6CNYtbgEDtQV22/9RBA=
2+
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
3+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0 h1:OYa9vmRX2XC5GXRAzeggG12sF/z5D9Ahtdm9EJ00WN4=
4+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
5+
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 h1:v9p9TfTbf7AwNb5NYQt7hI41IfPoLFiFkLtb+bmGjT0=
6+
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
7+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
111
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
212
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
3-
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
4-
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
13+
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
14+
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
15+
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
16+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
17+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
19+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
20+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
21+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
22+
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
23+
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
24+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
25+
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
26+
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
527
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
28+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
29+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
30+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
31+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
32+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
33+
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
34+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
35+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
36+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
39+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
40+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
41+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
42+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

‎tds.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ func (e *featureExtFedAuth) toBytes() []byte {
386386
var d []byte
387387

388388
switch e.FedAuthLibrary {
389-
case fedAuthLibrarySecurityToken:
389+
case FedAuthLibrarySecurityToken:
390390
d = make([]byte, 5)
391391
d[0] = options
392392

@@ -400,7 +400,7 @@ func (e *featureExtFedAuth) toBytes() []byte {
400400
d = append(d, e.Nonce...)
401401
}
402402

403-
case fedAuthLibraryADAL:
403+
case FedAuthLibraryADAL:
404404
d = []byte{options, e.ADALWorkflow}
405405
}
406406

@@ -930,7 +930,7 @@ func preparePreloginFields(p msdsn.Config, fe *featureExtFedAuth) map[uint8][]by
930930
preloginMARS: {0}, // MARS disabled
931931
}
932932

933-
if fe.FedAuthLibrary != fedAuthLibraryReserved {
933+
if fe.FedAuthLibrary != FedAuthLibraryReserved {
934934
fields[preloginFEDAUTHREQUIRED] = []byte{1}
935935
}
936936

@@ -948,7 +948,7 @@ func interpretPreloginResponse(p msdsn.Config, fe *featureExtFedAuth, fields map
948948

949949
// We need to be able to echo the value back to the server
950950
fe.FedAuthEcho = fedAuthSupport[0] != 0
951-
} else if fe.FedAuthLibrary != fedAuthLibraryReserved {
951+
} else if fe.FedAuthLibrary != FedAuthLibraryReserved {
952952
return 0, fmt.Errorf("federated authentication is not supported by the server")
953953
}
954954

@@ -987,7 +987,7 @@ func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger Cont
987987
TypeFlags: typeFlags,
988988
}
989989
switch {
990-
case fe.FedAuthLibrary == fedAuthLibrarySecurityToken:
990+
case fe.FedAuthLibrary == FedAuthLibrarySecurityToken:
991991
if uint64(p.LogFlags)&logDebug != 0 {
992992
logger.Log(ctx, msdsn.LogDebug, "Starting federated authentication using security token")
993993
}
@@ -1002,7 +1002,7 @@ func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, logger Cont
10021002

10031003
l.FeatureExt.Add(fe)
10041004

1005-
case fe.FedAuthLibrary == fedAuthLibraryADAL:
1005+
case fe.FedAuthLibrary == FedAuthLibraryADAL:
10061006
if uint64(p.LogFlags)&logDebug != 0 {
10071007
logger.Log(ctx, msdsn.LogDebug, "Starting federated authentication using ADAL")
10081008
}
@@ -1103,7 +1103,7 @@ initiate_connection:
11031103
}
11041104

11051105
fedAuth := &featureExtFedAuth{
1106-
FedAuthLibrary: fedAuthLibraryReserved,
1106+
FedAuthLibrary: FedAuthLibraryReserved,
11071107
}
11081108
if c.fedAuthRequired {
11091109
fedAuth.FedAuthLibrary = c.fedAuthLibrary

‎tds_login_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func TestLoginWithSecurityTokenAuth(t *testing.T) {
166166
if err != nil {
167167
t.Fatal(err)
168168
}
169-
conn, err := newSecurityTokenConnector(config,
169+
conn, err := NewSecurityTokenConnector(config,
170170
func(ctx context.Context) (string, error) {
171171
return "<token>", nil
172172
},
@@ -227,9 +227,9 @@ func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
227227
if err != nil {
228228
t.Fatal(err)
229229
}
230-
conn, err := newActiveDirectoryTokenConnector(
230+
conn, err := NewActiveDirectoryTokenConnector(
231231
config,
232-
fedAuthADALWorkflowPassword,
232+
FedAuthADALWorkflowPassword,
233233
func(ctx context.Context, serverSPN, stsURL string) (string, error) {
234234
return "<token>", nil
235235
},
@@ -301,9 +301,9 @@ func TestLoginWithADALManagedIdentityAuth(t *testing.T) {
301301
if err != nil {
302302
t.Fatal(err)
303303
}
304-
conn, err := newActiveDirectoryTokenConnector(
304+
conn, err := NewActiveDirectoryTokenConnector(
305305
config,
306-
fedAuthADALWorkflowMSI,
306+
FedAuthADALWorkflowMSI,
307307
func(ctx context.Context, serverSPN, stsURL string) (string, error) {
308308
return "<token>", nil
309309
},

‎tds_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestConstantsDefined(t *testing.T) {
4141
}
4242

4343
for _, i := range []int{
44-
fedAuthLibraryLiveIDCompactToken, fChangePassword, fSendYukonBinaryXML,
44+
FedAuthLibraryLiveIDCompactToken, fChangePassword, fSendYukonBinaryXML,
4545
} {
4646
if i < 0 {
4747
t.Fail()
@@ -120,7 +120,7 @@ func TestSendLoginWithFeatureExt(t *testing.T) {
120120
ClientLCID: 0x204,
121121
}
122122
login.FeatureExt.Add(&featureExtFedAuth{
123-
FedAuthLibrary: fedAuthLibrarySecurityToken,
123+
FedAuthLibrary: FedAuthLibrarySecurityToken,
124124
FedAuthToken: "fedauthtoken",
125125
})
126126
err := sendLogin(buf, &login)

0 commit comments

Comments
 (0)
Please sign in to comment.