Skip to content

Commit

Permalink
Update documentation for Azure AD authentication and add examples
Browse files Browse the repository at this point in the history
  • Loading branch information
wrosenuance committed Dec 15, 2020
1 parent a20cde6 commit 218b70d
Show file tree
Hide file tree
Showing 15 changed files with 966 additions and 29 deletions.
82 changes: 65 additions & 17 deletions README.md
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 PEM file containing the certificate and RSA private key in the `ClientCertPath` parameter, and set the `password` to the value needed to decrypt the RSA private key (leave blank if unencrypted).
* `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 `user@domain.com` 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
182 changes: 182 additions & 0 deletions doc/how-to-test-azure-ad-authentication.md
@@ -0,0 +1,182 @@
# How to test Azure AD authentication

To test Azure AD authentication requires an Azure SQL server configured with an
[Active Directory administrator](https://docs.microsoft.com/en-us/azure/sql-database/sql-database-aad-authentication-configure).
To test managed identity authentication, an Azure virtual machine configured with
[system-assigned and/or user-assigned identities](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm)
is also required.

The necessary resources can be set up through any means including the
[Azure Portal](https://portal.azure.com/), the Azure CLI, the Azure PowerShell cmdlets or
[Terraform](https://terraform.io/). To support these instructions, use the Terraform script at
[examples/azuread/testing.tf](../examples/azuread/testing.tf).

## Create Azure infrastructure

Download [Terraform](https://terraform.io/) to a location on your PATH.

Log in to Azure using the Azure CLI.

```console
you@workstation:~$ az login
you@workstation:~$ az account show
```

If your Azure account has access to multiple subscriptions, use
`az account set --subscription <name-or-ID>` to choose the correct one. You will need to have at
least Contributor access to the portal and permissions in Azure Active Directory to create users
and grants.

Check out this source repository (if you haven't already), change to the `examples/azuread`
directory and run Terraform:

```console
you@workstation:~$ git clone -b azure-auth https://github.com/wrosenuance/go-mssqldb.git
you@workstation:~$ cd go-mssqldb/examples/azuread
you@workstation:azuread$ terraform init
you@workstation:azuread$ terraform apply
```

This will create an Azure resource group, a SQL server with a database, a virtual machine with a
system-assigned identity and user-assigned identity. Resources are named based on a random
prefix: to specify the prefix, use `terraform apply -var prefix=<alphanumeric-and-hyphens-ok>`.

Upon successful completion, Terraform will display some key details of the infrastructure that has
been created. This includes the SSH key to access the VM, the administrator account and password
for the Azure SQL server, and all the relevant resource names.

Save the settings to a JSON file:

```console
you@workstation:azuread$ terraform output -json > settings.json
```

Save the SSH private key to a file:

```console
you@workstation:azuread$ terraform output vm_user_ssh_private_key > ssh-identity
```

Copy the `settings.json` to the new VM:

```console
you@workstation:azuread$ eval "VM_ADMIN_NAME=$(terraform output vm_admin_name)"
you@workstation:azuread$ eval "VM_IP_ADDRESS=$(terraform output vm_ip_address)"
you@workstation:azuread$ scp -i ssh-identity settings.json "${VM_ADMIN_NAME}@${VM_IP_ADDRESS}:"
```

## Set up Azure Virtual Machine for testing

SSH to the new VM to continue setup:

```console
you@workstation:azuread$ ssh -i ssh-identity "${VM_ADMIN_NAME}@${VM_IP_ADDRESS}"
```

Once on the VM, update the system and install some basic packages:

```console
azureuser@azure-vm:~$ sudo apt update -y
azureuser@azure-vm:~$ sudo apt upgrade -y
azureuser@azure-vm:~$ sudo apt install -y git openssl jq build-essential
azureuser@azure-vm:~$ sudo snap install go --classic
```

Install the Azure CLI using the script as shown below, or follow the
[manual install instructions](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt):

```console
azureuser@azure-vm:~$ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
```

## Generate service principal certificate file

Log in to Azure on the VM and set the subscription:

```console
azureuser@azure-vm:~$ az login
azureuser@azure-vm:~$ az account set --subscription "$(jq -r '.subscription_id.value' settings.json)"
```

Use OpenSSL to create a new certificate and key in PEM format, using the :

```console
azureuser@azure-vm:~$ openssl rand -writerand ~/.rnd
azureuser@azure-vm:~$ openssl req -x509 -nodes -newkey rsa:4096 -keyout client.key -out client.crt \
-subj "/C=US/ST=MA/L=Boston/O=Global Security/OU=IT Department/CN=AD-SP"
azureuser@azure-vm:~$ openssl rsa -out client.pem -in client.key -aes256 \
-passout "pass:$(jq -r '.app_sp_client_secret.value' settings.json)"
azureuser@azure-vm:~$ cat client.crt >> client.pem
azureuser@azure-vm:~$ export APP_SP_CLIENT_CERT="$PWD/client.pem"
```

Use the Azure CLI to add the client certificate to the application service principal:

```console
azureuser@azure-vm:~$ az ad sp credential reset --append --cert @client.crt \
--name "$(jq -r '.app_sp_client_id.value' settings.json)"
```

## Build source code and authorize users in database

Clone this repository, build and run the `examples/azuread` helper that verifies the database
exists and sets up access for the system-assigned and user-assigned identities.

```console
azureuser@azure-vm:~$ git clone -b azure-auth https://github.com/wrosenuance/go-mssqldb.git
azureuser@azure-vm:~$ cd go-mssqldb
azureuser@azure-vm:go-mssqldb$ go generate ./...
azureuser@azure-vm:go-mssqldb$ (cd ./examples/azuread; go build -o ../../azuread-example .)
azureuser@azure-vm:go-mssqldb$ eval "$(jq -r -f examples/azuread/environment-settings.jq ../settings.json)"
azureuser@azure-vm:go-mssqldb$ ./azuread-example -fedauth ActiveDirectoryPassword
```

For some basic connectivity tests, use the `examples/simple` helper. Run these commands on the
Azure VM so that identity authentication is possible.

```console
azureuser@azure-vm:go-mssqldb$ go build -o simple ./examples/simple
azureuser@azure-vm:go-mssqldb$ jq -r -f examples/azuread/ad-user-password-dsn.jq ../settings.json |
xargs ./simple -debug -dsn
azureuser@azure-vm:go-mssqldb$ jq -r -f examples/azuread/ad-service-principal-dsn.jq ../settings.json |
xargs ./simple -debug -dsn
azureuser@azure-vm:go-mssqldb$ jq -r --arg certpath "$APP_SP_CLIENT_CERT" \
-f examples/azuread/ad-service-principal-cert-dsn.jq ../settings.json |
xargs ./simple -debug -dsn
azureuser@azure-vm:go-mssqldb$ jq -r -f examples/azuread/ad-system-assigned-id-dsn.jq ../settings.json |
xargs ./simple -debug -dsn
azureuser@azure-vm:go-mssqldb$ jq -r -f examples/azuread/ad-user-assigned-id-dsn.jq ../settings.json |
xargs ./simple -debug -dsn
```

## Running the integration tests

Now that your environment is configured, you can run `go test`:

```console
azureuser@azure-vm:go-mssqldb$ export SQLSERVER_DSN="$(jq -r -f examples/azuread/sql-user-password-dsn.jq ../settings.json)"
azureuser@azure-vm:go-mssqldb$ go test -coverprofile=coverage.out ./...
```

## Tear down environment

After you complete your testing, use Terraform to destroy the infrastructure you created.

```console
you@workstation:azuread$ terraform destroy
```

## Troubleshooting

After Terraform runs you should be able to see resources that were created in the
[Azure Portal](https://portal.azure.com/).

If the Azure SQL server is successfully created you can connect to it using the AD admin user
and password in SSMS. SSMS will prompt you to create firewall rules if they are missing. You
can read the AD admin user and password from the `settings.json`, or run:

```console
you@workstation:azuread$ terraform output sql_ad_admin_user
you@workstation:azuread$ terraform output sql_ad_admin_password
```

2 changes: 2 additions & 0 deletions examples/azuread/.gitignore
@@ -0,0 +1,2 @@
settings.json
ssh-identity
1 change: 1 addition & 0 deletions examples/azuread/ad-service-principal-cert-dsn.jq
@@ -0,0 +1 @@
@uri "sqlserver://\(.app_sp_client_id.value)%40\(.tenant_id.value):\(.app_sp_client_secret.value)@\(.sql_server_fqdn.value)?database=\(.sql_database_name.value)&encrypt=true&fedauth=ActiveDirectoryApplication&clientcertpath=\($certpath)"
1 change: 1 addition & 0 deletions examples/azuread/ad-service-principal-dsn.jq
@@ -0,0 +1 @@
@uri "sqlserver://\(.app_sp_client_id.value)%40\(.tenant_id.value):\(.app_sp_client_secret.value)@\(.sql_server_fqdn.value)?database=\(.sql_database_name.value)&encrypt=true&fedauth=ActiveDirectoryApplication"
1 change: 1 addition & 0 deletions examples/azuread/ad-system-assigned-id-dsn.jq
@@ -0,0 +1 @@
@uri "sqlserver://\(.sql_server_fqdn.value)?database=\(.sql_database_name.value)&encrypt=true&fedauth=ActiveDirectoryMSI"
1 change: 1 addition & 0 deletions examples/azuread/ad-user-assigned-id-dsn.jq
@@ -0,0 +1 @@
@uri "sqlserver://\(.user_assigned_identity_client_id.value)@\(.sql_server_fqdn.value)?database=\(.sql_database_name.value)&encrypt=true&fedauth=ActiveDirectoryMSI"
1 change: 1 addition & 0 deletions examples/azuread/ad-user-password-dsn.jq
@@ -0,0 +1 @@
@uri "sqlserver://\(.sql_ad_admin_user.value):\(.sql_ad_admin_password.value)@\(.sql_server_fqdn.value)?database=\(.sql_database_name.value)&encrypt=true&fedauth=ActiveDirectoryPassword"

0 comments on commit 218b70d

Please sign in to comment.