Skip to content

Commit

Permalink
Added support for Azure Active Directory in DSN and via Connector.
Browse files Browse the repository at this point in the history
  • Loading branch information
wrosenuance committed Feb 3, 2020
1 parent 0f454e2 commit 948841d
Show file tree
Hide file tree
Showing 29 changed files with 2,116 additions and 328 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform
*.tfstate*
*.log
*.swp
*~
82 changes: 65 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ Other supported formats are listed below.
* true - Server certificate is not checked. Default is true if encrypt is not specified. If trust server certificate is true, driver accepts any certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.
* `certificate` - The file that contains the public key certificate of the CA that signed the SQL Server certificate. The specified certificate overrides the go platform specific CA certificates.
* `hostNameInCertificate` - Specifies the Common Name (CN) in the server certificate. Default value is the server host.
* `ServerSPN` - The kerberos SPN (Service Principal Name) for the server. Default is MSSQLSvc/host:port.
* `ServerSPN` - The Kerberos SPN (Service Principal Name) for the server. Default is MSSQLSvc/host:port.
* `Workstation ID` - The workstation name (default is the host name)
* `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener. The `database` must be specified when connecting with `Application Intent` set to `ReadOnly`.

* `FedAuth` - The federated authentication scheme to use. See below for additional setup requirements.
* `ActiveDirectoryApplication` - authenticates using an Azure Active Directory application client ID and client secret or certificate. Set the `user` to `client-ID@tenant-ID` and the `password` to the client secret. If using client certificates, provide the path to the PKCS#12 file containing the certificate and RSA private key in the `ClientCertPath` parameter, and set the `password` to the value needed to open the PKCS#12 file.
* `ActiveDirectoryMSI` - authenticates using the managed service identity (MSI) attached to the VM, or a specific user-assigned identity if a client ID is specified in the `user` field.
* `ActiveDirectoryPassword` - authenticates an Azure Active Directory user account in the form `[email protected]` with a password. This method is not recommended for general use and does not support multi-factor authentication for accounts.
* `ActiveDirectoryIntegrated` - configures the connection to request Active Directory Integrated authentication. This method is not fully supported: you must also implement a token provider to obtain the token for the currently logged-in user and supply it in the `ActiveDirectoryTokenProvider` field in the `Connector` as described below.

### The connection string can be specified in one of three formats:


Expand Down Expand Up @@ -106,25 +111,68 @@ Other supported formats are listed below.
* `odbc:server=localhost;user id=sa;password={foo{bar}` // Literal `{`, password is "foo{bar"
* `odbc:server=localhost;user id=sa;password={foo}}bar}` // Escaped `} with `}}`, password is "foo}bar"

### Azure Active Directory authentication - preview
### Azure Active Directory authentication

Azure Active Directory authentication uses temporary authentication tokens to authenticate.
To have the driver obtain these tokens using the
[Active Directory Authentication Library for Go](https://github.com/Azure/go-autorest/tree/master/autorest/adal),
import the Azure AD module in addition to the normal driver module, and configure the
connection string with a `FedAuth` option and supporting information as described above.

```golang
import (
"database/sql"
"net/url"

The configuration of functionality might change in the future.
// Import the Azure AD driver module (also imports the regular driver package)
_ "github.com/denisenkom/go-mssqldb/azuread"
)

func ConnectWithMSI() (*sql.DB, error) {
return sql.Open("sqlserver", "sqlserver://azuresql.database.windows.net?database=yourdb&fedauth=ActiveDirectoryMSI")
}
```

As an alternative, you can select the federated authentication library and Active Directory
using the connection string parameters, but then implement your own routine for obtaining
tokens.

```golang
import (
"context"
"database/sql"
"net/url"

// Import the driver
"github.com/denisenkom/go-mssqldb"
)

func ConnectWithADToken() (*sql.DB, error) {
conn, err := mssql.NewConnector("sqlserver://azuresql.database.windows.net?database=yourdb&fedauth=ActiveDirectoryApplication")
if err != nil {
// handle errors in DSN
}

conn.SecurityTokenProvider = func(ctx context.Context) (string, error) {
return "the token", nil
}

return sql.OpenDB(conn), nil
}

func ConnectWithADIntegrated() (*sql.DB, error) {
conn, err := mssql.NewConnector("sqlserver://azuresq;.database.windows.net?database=yourdb&fedauth=ActiveDirectoryIntegrated")
if err != nil {
// handle errors in DSN
}

c.ActiveDirectoryTokenProvider = func(ctx context.Context, serverSPN, stsURL string) (string, error) {
return "the token", nil
}

Azure Active Directory (AAD) access tokens are relatively short lived and need to be
valid when a new connection is made. Authentication is supported using a callback func that
provides a fresh and valid token using a connector:
``` golang
conn, err := mssql.NewAccessTokenConnector(
"Server=test.database.windows.net;Database=testdb",
tokenProvider)
if err != nil {
// handle errors in DSN
return sql.OpenDB(conn), nil
}
db := sql.OpenDB(conn)
```
Where `tokenProvider` is a function that returns a fresh access token or an error. None of these statements
actually trigger the retrieval of a token, this happens when the first statment is issued and a connection
is created.

## Executing Stored Procedures

Expand Down
28 changes: 3 additions & 25 deletions accesstokenconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,8 @@ import (
"context"
"database/sql/driver"
"errors"
"fmt"
)

var _ driver.Connector = &accessTokenConnector{}

// accessTokenConnector wraps Connector and injects a
// fresh access token when connecting to the database
type accessTokenConnector struct {
Connector

accessTokenProvider func() (string, error)
}

// NewAccessTokenConnector creates a new connector from a DSN and a token provider.
// The token provider func will be called when a new connection is requested and should return a valid access token.
// The returned connector may be used with sql.OpenDB.
Expand All @@ -32,20 +21,9 @@ func NewAccessTokenConnector(dsn string, tokenProvider func() (string, error)) (
return nil, err
}

c := &accessTokenConnector{
Connector: *conn,
accessTokenProvider: tokenProvider,
}
return c, nil
}

// Connect returns a new database connection
func (c *accessTokenConnector) Connect(ctx context.Context) (driver.Conn, error) {
var err error
c.Connector.params.fedAuthAccessToken, err = c.accessTokenProvider()
if err != nil {
return nil, fmt.Errorf("mssql: error retrieving access token: %+v", err)
conn.SecurityTokenProvider = func(ctx context.Context) (string, error) {
return tokenProvider()
}

return c.Connector.Connect(ctx)
return conn, nil
}
10 changes: 5 additions & 5 deletions accesstokenconnector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ func TestNewAccessTokenConnector(t *testing.T) {
dsn: dsn,
tokenProvider: tp},
want: func(c driver.Connector) error {
tc, ok := c.(*accessTokenConnector)
tc, ok := c.(*Connector)
if !ok {
return fmt.Errorf("Expected driver to be of type *accessTokenConnector, but got %T", c)
return fmt.Errorf("Expected driver to be of type *Connector, but got %T", c)
}
p := tc.Connector.params
p := tc.params
if p.database != "db" {
return fmt.Errorf("expected params.database=db, but got %v", p.database)
}
if p.host != "server.database.windows.net" {
return fmt.Errorf("expected params.host=server.database.windows.net, but got %v", p.host)
}
if tc.accessTokenProvider == nil {
if tc.SecurityTokenProvider == nil {
return fmt.Errorf("Expected tokenProvider to not be nil")
}
t, err := tc.accessTokenProvider()
t, err := tc.SecurityTokenProvider(context.TODO())
if t != "token" || err != nil {
return fmt.Errorf("Unexpected results from tokenProvider: %v, %v", t, err)
}
Expand Down
2 changes: 2 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ install:
- go version
- go env
- go get -u github.com/golang-sql/civil
- go get -u golang.org/x/crypto/pkcs12
- go get -u github.com/Azure/go-autorest/autorest/adal

build_script:
- go build
Expand Down
136 changes: 136 additions & 0 deletions azuread/fedauth_adal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package azuread

import (
"context"
"crypto/rsa"
"crypto/x509"
"fmt"

"github.com/Azure/go-autorest/autorest/adal"
mssql "github.com/denisenkom/go-mssqldb"
)

type azureFedAuthConfigurer struct{}

func init() {
mssql.SetFederatedAuthenticationConfigurer(&azureFedAuthConfigurer{})
}

// When the security token library is used, the token is obtained without input
// from the server, so the AD endpoint and Azure SQL resource URI are provided
// from the constants below.
const (
// activeDirectoryEndpoint is the security token service URL to use when
// the server does not provide the URL.
activeDirectoryEndpoint = "https://login.microsoftonline.com/"

// azureSQLResource is the AD resource to use when the server does not
// provide the resource.
azureSQLResource = "https://database.windows.net/"

// driverClientID is the AD client ID to use when performing a username
// and password login.
driverClientID = "7f98cb04-cd1e-40df-9140-3bf7e2cea4db"
)

func retrieveToken(ctx context.Context, token *adal.ServicePrincipalToken) (string, error) {
err := token.RefreshWithContext(ctx)
if err != nil {
err = fmt.Errorf("Failed to refresh token: %v", err)
return "", err
}

return token.Token().AccessToken, nil
}

func (az *azureFedAuthConfigurer) SecurityTokenProviderFromCertificate(clientID, tenantID string, certificate *x509.Certificate, rsaPrivateKey *rsa.PrivateKey) mssql.SecurityTokenProvider {
return func(ctx context.Context) (string, error) {
// The activeDirectoryEndpoint URL is used as a base against which the
// tenant ID is resolved.
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, tenantID)
if err != nil {
err = fmt.Errorf("Failed to obtain OAuth configuration for endpoint %s and tenant %s: %v",
activeDirectoryEndpoint, tenantID, err)
return "", err
}

token, err := adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, clientID, certificate, rsaPrivateKey, azureSQLResource)
if err != nil {
err = fmt.Errorf("Failed to obtain service principal token for client id %s in tenant %s: %v", clientID, tenantID, err)
return "", err
}

return retrieveToken(ctx, token)
}
}

func (az *azureFedAuthConfigurer) SecurityTokenProviderFromSecret(clientID, tenantID, clientSecret string) mssql.SecurityTokenProvider {
return func(ctx context.Context) (string, error) {
// The activeDirectoryEndpoint URL is used as a base against which the
// tenant ID is resolved.
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, tenantID)
if err != nil {
err = fmt.Errorf("Failed to obtain OAuth configuration for endpoint %s and tenant %s: %v",
activeDirectoryEndpoint, tenantID, err)
return "", err
}

token, err := adal.NewServicePrincipalToken(*oauthConfig, clientID, clientSecret, azureSQLResource)

if err != nil {
err = fmt.Errorf("Failed to obtain service principal token for client id %s in tenant %s: %v", clientID, tenantID, err)
return "", err
}

return retrieveToken(ctx, token)
}
}

func (az *azureFedAuthConfigurer) ActiveDirectoryTokenProviderFromPassword(user, password string) mssql.ActiveDirectoryTokenProvider {
return func(ctx context.Context, serverSPN, stsURL string) (string, error) {
// The activeDirectoryEndpoint URL is used as a base against which the
// STS URL is resolved. However, the STS URL is normally absolute and
// the activeDirectoryEndpoint URL is completely ignored.
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, stsURL)
if err != nil {
err = fmt.Errorf("Failed to obtain OAuth configuration for endpoint %s and tenant %s: %v",
activeDirectoryEndpoint, stsURL, err)
return "", err
}

token, err := adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, driverClientID, user, password, serverSPN)

if err != nil {
err = fmt.Errorf("Failed to obtain token for user %s for resource %s from service %s: %v", user, serverSPN, stsURL, err)
return "", err
}

return retrieveToken(ctx, token)
}
}

func (az *azureFedAuthConfigurer) ActiveDirectoryTokenProviderFromIdentity(clientID string) mssql.ActiveDirectoryTokenProvider {
return func(ctx context.Context, serverSPN, stsURL string) (string, error) {
msiEndpoint, err := adal.GetMSIEndpoint()
if err != nil {
return "", err
}

var token *adal.ServicePrincipalToken
var access string
if clientID == "" {
access = "system identity"
token, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, serverSPN)
} else {
access = "user-assigned identity " + clientID
token, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, serverSPN, clientID)
}

if err != nil {
err = fmt.Errorf("Failed to obtain token for %s for resource %s from service %s: %v", access, serverSPN, stsURL, err)
return "", err
}

return retrieveToken(ctx, token)
}
}
Loading

0 comments on commit 948841d

Please sign in to comment.