Authenticate Go apps to Azure services during local development using service principals

When creating cloud applications, developers need to debug and test applications on their local workstation. When an application is run on a developer's workstation during local development, it still must authenticate to any Azure services used by the app. This article covers how to set up dedicated application service principal objects to be used during local development.

A diagram showing how an app running in local developer obtains the application service principal from an .env file and then uses that identity to connect to Azure resources.

Dedicated application service principals for local development allow you to follow the principle of least privilege during app development. Since permissions are scoped to exactly what is needed for the app during development, app code is prevented from accidentally accessing an Azure resource intended for use by a different app. This also prevents bugs from occurring when the app is moved to production because the app was overprivileged in the dev environment.

An application service principal is set up for the app when the app is registered in Azure. When registering apps for local development, it's recommended to:

  • Create separate app registrations for each developer working on the app. This will create separate application service principals for each developer to use during local development and avoid the need for developers to share credentials for a single application service principal.
  • Create separate app registrations per app. This scopes the app's permissions to only what is needed by the app.

During local development, environment variables are set with the application service principal's identity. The Azure SDK for Go reads these environment variables and uses this information to authenticate the app to the Azure resources it needs.

1 - Register the application in Azure

Application service principal objects are created with an app registration in Azure. This can be done using either the Azure portal or Azure CLI.

Azure CLI commands can be run in the Azure Cloud Shell or on a workstation with the Azure CLI installed.

First, use the az ad sp create-for-rbac command to create a new service principal for the app. The command also creates the app registration for the app at the same time.

az ad sp create-for-rbac --name <service-principal-name>

The output of this command will look like the following. Make note of these values or keep this window open as you'll need these values in the next steps and won't be able to view the password (client secret) value again. You can, however, add a new password later without invalidating the service principal or existing passwords if needed.

{
  "appId": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "displayName": "<service-principal-name>",
  "password": "Ee5Ff~6Gg7.-Hh8Ii9Jj0Kk1Ll2Mm3_Nn4Oo5Pp6",
  "tenant": "aaaabbbb-0000-cccc-1111-dddd2222eeee"
}

2 - Create a Microsoft Entra security group for local development

Since there are typically multiple developers who work on an application, it's recommended to create a Microsoft Entra security group to encapsulate the roles (permissions) the app needs in local development, rather than assigning the roles to individual service principal objects. This offers the following advantages:

  • Every developer is assured to have the same roles assigned since roles are assigned at the group level.
  • If a new role is needed for the app, it only needs to be added to the Microsoft Entra group for the app.
  • If a new developer joins the team, a new application service principal is created for the developer and added to the group, assuring the developer has the right permissions to work on the app.

The az ad group create command is used to create security groups in Microsoft Entra ID. The --display-name and --main-nickname parameters are required. The name given to the group should be based on the name of the application. It's also useful to include a phrase like 'local-dev' in the name of the group to indicate the purpose of the group.

az ad group create \
    --display-name MyDisplay \
    --mail-nickname MyDisplay  \
    --description "<group-description>"

Copy the value of the id property in the output of the command. This is the object ID for the group. You need it in later steps. You can also use the az ad group show command to retrieve this property.

To add members to the group, you need the object ID of the application service principal, which is different than the application ID. Use the az ad sp list to list the available service principals. The --filter parameter command accepts OData style filters and can be used to filter the list as shown. The --query parameter limits to columns to only those of interest.

az ad sp list \
    --filter "startswith(displayName, 'msdocs')" \
    --query "[].{objectId:id, displayName:displayName}" \
    --output table

The az ad group member add command can then be used to add members to groups.

az ad group member add \
    --group <group-name> \
    --member-id <object-id>

Note

By default, the creation of Microsoft Entra security groups is limited to certain privileged roles in a directory. If you're unable to create a group, contact an administrator for your directory. If you're unable to add members to an existing group, contact the group owner or a directory administrator. To learn more, see Manage Microsoft Entra groups and group membership.

3 - Assign roles to the application

Next, you need to determine what roles (permissions) your app needs on what resources and assign those roles to your app. In this example, the roles are assigned to the Microsoft Entra group created in step 2. Roles can be assigned at a resource, resource group, or subscription scope. This example shows how to assign roles at the resource group scope since most applications group all their Azure resources into a single resource group.

A user, group, or application service principal is assigned a role in Azure using the az role assignment create command. You can specify a group with its object ID. You can specify an application service principal with its appId.

az role assignment create --assignee <appId or objectId> \
    --scope /subscriptions/<subscriptionId>/resourceGroups/<resourceGroupName> \
    --role "<roleName>" 

To get the role names that can be assigned, use the az role definition list command.

az role definition list \
    --query "sort_by([].{roleName:roleName, description:description}, &roleName)" \
    --output table

For example, to allow the application service principal with the appId of 00001111-aaaa-2222-bbbb-3333cccc4444 read, write, and delete access to Azure Storage blob containers and data in all storage accounts in the msdocs-go-sdk-auth-example resource group in the subscription with ID aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e, you would assign the application service principal to the Storage Blob Data Contributor role using the following command.

az role assignment create --assignee 00001111-aaaa-2222-bbbb-3333cccc4444 \
    --scope /subscriptions/aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e/resourceGroups/msdocs-go-sdk-auth-example \
    --role "Storage Blob Data Contributor"

For information on assigning permissions at the resource or subscription level using the Azure CLI, see the article Assign Azure roles using the Azure CLI.

4 - Set local development environment variables

The DefaultAzureCredential object will look for the service principal information in a set of environment variables at runtime. Since most developers work on multiple applications, it's recommended to use a package like godotenv to access environment from a .env file stored in the application's directory during development. This scopes the environment variables used to authenticate the application to Azure such that they can only be used by this application.

The .env file is never checked into source control since it contains the application secret key for Azure. The standard .gitignore file for Go automatically excludes the .env file from check-in.

To use the godotenv package, first install the package in your application.

go get github.com/joho/godotenv

Then, create a .env file in your application root directory. Set the environment variable values with values obtained from the app registration process as follows:

  • AZURE_CLIENT_ID → The app ID value.
  • AZURE_TENANT_ID → The tenant ID value.
  • AZURE_CLIENT_SECRET → The password/credential generated for the app.
AZURE_CLIENT_ID=00001111-aaaa-2222-bbbb-3333cccc4444
AZURE_TENANT_ID=aaaabbbb-0000-cccc-1111-dddd2222eeee
AZURE_CLIENT_SECRET=Ee5Ff~6Gg7.-Hh8Ii9Jj0Kk1Ll2Mm3_Nn4Oo5Pp6

Finally, in the startup code for your application, use the godotenv library to read the environment variables from the .env file on startup.

// Imports of fmt, log, and os omitted for brevity 
import "github.com/joho/godotenv"

environment := os.Getenv("ENVIRONMENT")

if environment == "development" {
    fmt.Println("Loading environment variables from .env file")
    
    // Load the .env file
    err := godotenv.Load(".env")
    if err != nil {
        log.Fatalf("Error loading .env file: %v", err)
    }
}

5 - Implement DefaultAzureCredential in your application

To authenticate Azure SDK client objects to Azure, your application should use the DefaultAzureCredential class from the azidentity package. In this scenario, DefaultAzureCredential will detect the environment variables AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET are set and read those variables to get the application service principal information to connect to Azure with.

First, add the azidentity package to your application.

go get github.com/Azure/azure-sdk-for-go/sdk/azidentity

Next, for any Go code that creates an Azure SDK client object in your app, you'll want to:

  1. Import the azidentity package.
  2. Create an instance of DefaultAzureCredential type.
  3. Pass the instance of DefaultAzureCredential type to the Azure SDK client constructor.

An example of this is shown in the following code segment.

import (
	"context"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
)

const (
	account       = "https://<replace_with_your_storage_account_name>.blob.core.windows.net/"
	containerName = "sample-container"
	blobName      = "sample-blob"
	sampleFile    = "path/to/sample/file"
)

func main() {
    // create a credential
    cred, err := azidentity.NewDefaultAzureCredential(nil)
    if err != nil {
      // TODO: handle error
    }
    
    // create a client for the specified storage account
    client, err := azblob.NewClient(account, cred, nil)
    if err != nil {
      // TODO: handle error
    }
    
    // TODO: perform some action with the azblob Client
    // _, err = client.DownloadFile(context.TODO(), <containerName>, <blobName>, <target_file>, <DownloadFileOptions>)
}