Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added support for federated authentication to enable Azure AD authentication #547

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 80 additions & 16 deletions README.md
Expand Up @@ -54,10 +54,11 @@ 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`.


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


Expand Down Expand Up @@ -106,25 +107,88 @@ 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

The configuration of functionality might change in the future.
Azure Active Directory authentication uses temporary authentication tokens to authenticate.
The `mssql` package does not provide an implementation to obtain tokens: instead, import the
`azuread` package and use driver name `azuresql`. This driver uses the
[Active Directory Authentication Library for Go](https://github.com/Azure/go-autorest/tree/master/autorest/adal)
to obtain Azure Active Directory authentication tokens.

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
Authentication using Active Directory is enabled using the `fedauth` connection parameter,
in combination with the `user id` and `password` (or URL username and password).

* `fedauth=ActiveDirectoryApplication` - authenticates using an Azure Active Directory application client ID and client secret or certificate.

Set the `user id` to `clientID@tenantID` for your service principal. If using a client secret, set the `password` to the client secret. If using client certificates, provide the path to the PEM file containing the certificate concatenated with the RSA private key in the `clientcertpath` parameter, and set the `password` to the passphrase needed to decrypt the RSA private key (omit or leave blank if unencrypted).

* `fedauth=ActiveDirectoryMSI` - authenticates using the managed service identity (MSI) attached to the VM (system identity), or a specific user-assigned identity.

To select a user-assigned identity, specify a client ID in the `user id` parameter. For the system-assigned identity, leave the `user id` empty.

* `fedauth=ActiveDirectoryPassword` - authenticates an Azure Active Directory user account.

Set the `user id` to `user@domain.com` and the `password`. This method is not recommended for general use and does not support multi-factor authentication for accounts.


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

// 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(azuread.DriverName, "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. The second example shows how this could be used to add in a token for the Azure AD
Integrated authentication scenario.

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

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

func ConnectWithSecurityToken() (*sql.DB, error) {
conn, err := mssql.NewSecurityTokenConnector(
"sqlserver://azuresql.database.windows.net?database=yourdb",
func(ctx context.Context) (string, error) {
return "the token", nil
},
)
if err != nil {
// handle errors in DSN
}

return sql.OpenDB(conn), nil
}

func ConnectWithADIntegrated() (*sql.DB, error) {
conn, err := mssql.NewActiveDirectoryTokenConnector(
"sqlserver://azuresq;.database.windows.net?database=yourdb",
2, // Active Directory workflow: 1 = user/password, 2 = integrated, 3 = MSI
func(ctx context.Context, serverSPN, stsURL string) (string, error) {
return "the token", nil
},
)
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
3 changes: 3 additions & 0 deletions appveyor.yml
Expand Up @@ -12,8 +12,10 @@ environment:
SQLPASSWORD: Password12!
DATABASE: test
GOVERSION: 113
ADALSUPPORT: test
matrix:
- GOVERSION: 18
ADALSUPPORT: no-test
SQLINSTANCE: SQL2017
- GOVERSION: 19
SQLINSTANCE: SQL2017
Expand Down Expand Up @@ -46,6 +48,7 @@ install:
- go version
- go env
- go get -u github.com/golang-sql/civil
- if %ADALSUPPORT%==test go get -u github.com/Azure/go-autorest/autorest/adal

build_script:
- go build
Expand Down
135 changes: 135 additions & 0 deletions azuread/adal_tokens.go
@@ -0,0 +1,135 @@
package azuread

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

"github.com/Azure/go-autorest/autorest/adal"
)

// 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.
var (
// activeDirectoryEndpoint is the security token service URL to use when
// the server does not provide the URL.
activeDirectoryEndpoint = "https://login.microsoftonline.com/"
)

func init() {
endpoint := os.Getenv("AZURE_AD_STS_URL")
if endpoint != "" {
activeDirectoryEndpoint = endpoint
}
}

const (
// 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
}

// SecurityTokenFromCertificate obtains a security token using a certificate and RSA private key.
func SecurityTokenFromCertificate(ctx context.Context, clientID, tenantID string, certificate *x509.Certificate, privateKey *rsa.PrivateKey) (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, privateKey, 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)
}

// SecurityTokenFromSecret obtains a security token using a client ID and secret.
func SecurityTokenFromSecret(ctx context.Context, clientID, tenantID, clientSecret string) (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)
}

// ActiveDirectoryTokenFromPassword obtains a security token using an Active Directory username and password.
func ActiveDirectoryTokenFromPassword(ctx context.Context, serverSPN, stsURL, user, password 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)
}

// ActiveDirectoryTokenFromIdentity obtains a security token the managed identity service.
func ActiveDirectoryTokenFromIdentity(ctx context.Context, serverSPN, stsURL, clientID 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)
}