The Grayzone

Azure RM OAuth2 with Powershell

Background

For the past few months I’ve been working on better/fully automated deployments. Using a combination of TeamCity, Cake and Octopus Deploy we have our application deployment story pretty much sorted.

I’m now looking at automating deployment of our infrastructure in Azure. For this I’ve been using Terraform. As great as Terraform is, not all of the required Azure resources are currently supported so I had to write some Powershell scripts, using Azure RM cmdlets, to create these.

The unsupported resources that I needed to create was a vNet Gateway and connections for a vnet-to-vnet connection. I was loosely following the Azure vnet-to-vnet guide for creating the resources I needed. In that article a call to Login-AzureRmAccount is used for interactively authenticating, however as I’m looking to automate this it wasn’t an option so I chose to go with OAuth.

Azure OAuth2

I had already set up an application within Azure for use with the Terraform Azure Provider, so I figured the path of least resistance was to use the same oauth client credentials in my Powershell scripts, as this would enabled them to be run without any user interaction.

It’s worth noting that although I used this to augment my Terraform configuration it’s entirely possible to use the same method for creating resources purely through Powershell.

Azure OAuth with Powershell

I already had the following client credentials for Azure:

{
  "subscription_id": "snip",
  "client_id": "snip",
  "client_secret": "snip",
  "tenant_id": "snip"
}

See the above Terraform Azure Provider link for details on how to get these.

Get Access Token

The first thing I needed to do was use the client credentials to get an access token. This is done by making a POST request to https://login.microsoftonline.com/{tenantId}/oauth2/token?api-version=1.0 (documentation doesn’t mention appending api-version but I was getting an error without it).

The values that need to be used are:

$formData = @{
  client_id = $connection.client_id;
  client_secret = $connection.client_secret;
  scope = 'terraform/.default';
  grant_type = 'client_credentials';
  resource = 'https://management.core.windows.net/'
}

$uri = 'https://login.microsoftonline.com/' + $connection.tenant_id + '/oauth2/token?api-version=1.0'
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $formData -ContentType "application/x-www-form-urlencoded"
Write-Output $response.access_token # don't do this - only an example

On success this will return a 200 response with a body similar to:

{
  "token_type": "Bearer",
  "expires_in": "3599",
  "ext_expires_in": "0",
  "expires_on": "1388444763",
  "not_before": "1388445821",
  "resource": "https://management.core.windows.net/",
  "access_token": "my_access_token"
}

See the ‘get a token’ section of the Azure REST API docs for more information.

Access Token Lifetime

As you will see in the response body, there is an expiry associated with the token. For my use case I don’t require the token to be particularly long-lived, I will create it and immediately use it. It is possible to use refresh tokens to get new access tokens. See token lifetimes for more details.

Authenticate using Token

The access token can now be used to authenticate requests. For this I used the Add-AzureRmAccount cmdlet which will authenticate the account for the current PS session.

There are 8 different variations on parameters but the following is all that is needed:

Add-AzureRmAccount -AccessToken 'my_access_token' -AccountId $connection.client_id

You can test if the authentication has worked by running Get-AzureRmSubscription. If successful a list of all subscriptions for the account will be returned.

Errors

If the resource has not been set correctly for the provided token you will receive an error.

ERROR Add-AzureRmAccount : InvalidAuthenticationTokenAudience: The access token has been obtained from wrong audience or resource ‘spn:00000002-0000-0000-c000-000000000000’. It should exactly match (including forward slash) with one of the allowed audiences ‘https://management.core.windows.net/’,’https://management.azure.com/’. At line:1 char:1

  • Add-AzureRmAccount -AccessToken ‘abcdef1234567890 …
  • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    • CategoryInfo : CloseError: (:) [Add-AzureRmAccount], CloudException
    • FullyQualifiedErrorId : Microsoft.Azure.Commands.Profile.AddAzureRMAccountCommand

Most of the errors that I received were fairly informative and helped point me in the right direction.

REST API

The same access token can also be used for authorising request via the Azure REST API. To do this you would add a bearer authentication header to the request: Authorization : Bearer my_access_token

Create Resources

That’s it! Now that you have authenticated you can make requests as normal using the Azure RM cmdlets.

Additional Notes

Since I was using the same credentials for both Powershell and Terraform I stored the provider credentials as JSON rather than HCL. HCL is cleaner and easier to understand but JSON understood by Terraform and far easier to parse in Powershell.

An example of the 2 different formats are below:

JSON:

{
  "provider":{
    "azurerm":{
      "subscription_id": "snip",
      "client_id": "snip",
      "client_secret": "snip",
      "tenant_id": "snip"
    }
  }
}

HCL:

provider "azurerm" {
  subscription_id = "snip"
  client_id       = "snip"
  client_secret   = "snip"
  tenant_id       = "snip"
}

Share this: