Key Vault with the use of VBScript/Classic ASP

I have seen two instances recently where customers were asking about using VBScript/Classic ASP with Key Vault.  Key Vault does not have a library written for VBScript - it's a very old language and its use is not typical these days.  That said, there's always someone who needs to update a perfectly well-working application with some modern updates.

The great thing about Web APIs these days is that we've standardized on REST.  If you're reading this, you probably know what it is and I'm not going to get into it - you can read the Wikipedia article for more information.

That said, with every Key Vault action available as part of the REST API, we can consume Key Vault from every language that can perform HTTP requests.  I want to give a sample of VBScript retrieving a secret from Key Vault.  This code should work with very few modifications in Classic ASP as well.


Note: I've made use of an open-source ASP library called ASPJSON.  This is to simplify getting values from the returned JSON objects from Key Vault.  

' Written by Matthew Small on
' 12/15/2020

Option Explicit
dim xmlhttp, url, oJSON, accessToken, authUrl, kvUrl, clientId, clientSecret, secretValue
set xmlhttp = CreateObject("Microsoft.XMLHTTP")
set oJSON = New aspJSON

' Set constants
authUrl = "<your tenant here>/oauth2/token"
kvUrl = "https://<your kv here><secret name>?api-version=7.1"
clientId = <removed>
clientSecret = <removed>

' Obtain an access token using the Microsoft OAUTH services.  I need a registered application with a client secret to make this happen. "POST", authUrl, false
xmlhttp.setRequestHeader "Accept", "application/json"
xmlhttp.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
xmlhttp.setRequestHeader "Host", ""
xmlhttp.send "" & clientId & "&client_secret=" & clientSecret  & "&grant_type=client_credentials"

' Use the aspJSON class to get the access token from the response
accessToken ="access_token")

' Using the access token, call into the Key Vault to get the value of a secret. "GET", kvUrl, false
xmlhttp.setRequestHeader "Authorization", "Bearer " & accessToken

' Use the aspJSON class to get the secret value from the response and display it.
secretValue ="value")
WScript.echo "The value of the secret from Key Vault is: """ & secretValue & """."

' This is the aspJSON class obtained from GITHUB at  I modifed it slightly (Server.CreateObject -> CreateObject) for use in VBS.
Class aspJSON
	Public data
	Private p_JSONstring
	private aj_in_string, aj_in_escape, aj_i_tmp, aj_char_tmp, aj_s_tmp, aj_line_tmp, aj_line, aj_lines, aj_currentlevel, aj_currentkey, aj_currentvalue, aj_newlabel, aj_XmlHttp, aj_RegExp, aj_colonfound

	Private Sub Class_Initialize()
		Set data = Collection()

	    Set aj_RegExp = new regexp
	    aj_RegExp.Pattern = "\s{0,}(\S{1}[\s,\S]*\S{1})\s{0,}"
	    aj_RegExp.Global = False
	    aj_RegExp.IgnoreCase = True
	    aj_RegExp.Multiline = True
	End Sub

	Private Sub Class_Terminate()
		Set data = Nothing
	    Set aj_RegExp = Nothing
	End Sub

	Public Sub loadJSON(inputsource)
		inputsource = aj_MultilineTrim(inputsource)
		If Len(inputsource) = 0 Then Err.Raise 1, "loadJSON Error", "No data to load."
		select case Left(inputsource, 1)
			case "{", "["
			case else
				Set aj_XmlHttp = CreateObject("Msxml2.ServerXMLHTTP") "GET", inputsource, False
				aj_XmlHttp.setRequestHeader "Content-Type", "text/json"
				aj_XmlHttp.setRequestHeader "CharSet", "UTF-8"
				inputsource = aj_XmlHttp.responseText
				set aj_XmlHttp = Nothing
		end select

		p_JSONstring = CleanUpJSONstring(inputsource)
		aj_lines = Split(p_JSONstring, Chr(13) & Chr(10))

		Dim level(99)
		aj_currentlevel = 1
		Set level(aj_currentlevel) = data
		For Each aj_line In aj_lines
			aj_currentkey = ""
			aj_currentvalue = ""
			If Instr(aj_line, ":") > 0 Then
				aj_in_string = False
				aj_in_escape = False
				aj_colonfound = False
				For aj_i_tmp = 1 To Len(aj_line)
					If aj_in_escape Then
						aj_in_escape = False
						Select Case Mid(aj_line, aj_i_tmp, 1)
							Case """"
								aj_in_string = Not aj_in_string
							Case ":"
								If Not aj_in_escape And Not aj_in_string Then
									aj_currentkey = Left(aj_line, aj_i_tmp - 1)
									aj_currentvalue = Mid(aj_line, aj_i_tmp + 1)
									aj_colonfound = True
									Exit For
								End If
							Case "\"
								aj_in_escape = True
						End Select
					End If
				if aj_colonfound then
					aj_currentkey = aj_Strip(aj_JSONDecode(aj_currentkey), """")
					If Not level(aj_currentlevel).exists(aj_currentkey) Then level(aj_currentlevel).Add aj_currentkey, ""
				end if
			End If
			If right(aj_line,1) = "{" Or right(aj_line,1) = "[" Then
				If Len(aj_currentkey) = 0 Then aj_currentkey = level(aj_currentlevel).Count
				Set level(aj_currentlevel).Item(aj_currentkey) = Collection()
				Set level(aj_currentlevel + 1) = level(aj_currentlevel).Item(aj_currentkey)
				aj_currentlevel = aj_currentlevel + 1
				aj_currentkey = ""
			ElseIf right(aj_line,1) = "}" Or right(aj_line,1) = "]" or right(aj_line,2) = "}," Or right(aj_line,2) = "]," Then
				aj_currentlevel = aj_currentlevel - 1
			ElseIf Len(Trim(aj_line)) > 0 Then
				if Len(aj_currentvalue) = 0 Then aj_currentvalue = aj_line
				aj_currentvalue = getJSONValue(aj_currentvalue)

				If Len(aj_currentkey) = 0 Then aj_currentkey = level(aj_currentlevel).Count
				level(aj_currentlevel).Item(aj_currentkey) = aj_currentvalue
			End If
	End Sub

	Public Function Collection()
		set Collection = CreateObject("Scripting.Dictionary")
	End Function

	Public Function AddToCollection(dictobj)
		if TypeName(dictobj) <> "Dictionary" then Err.Raise 1, "AddToCollection Error", "Not a collection."
		aj_newlabel = dictobj.Count
		dictobj.Add aj_newlabel, Collection()
		set AddToCollection = dictobj.item(aj_newlabel)
	end function

	Private Function CleanUpJSONstring(aj_originalstring)
		aj_originalstring = Replace(aj_originalstring, Chr(13) & Chr(10), "")
		aj_originalstring = Mid(aj_originalstring, 2, Len(aj_originalstring) - 2)
		aj_in_string = False : aj_in_escape = False : aj_s_tmp = ""
		For aj_i_tmp = 1 To Len(aj_originalstring)
			aj_char_tmp = Mid(aj_originalstring, aj_i_tmp, 1)
			If aj_in_escape Then
				aj_in_escape = False
				aj_s_tmp = aj_s_tmp & aj_char_tmp
				Select Case aj_char_tmp
					Case "\" : aj_s_tmp = aj_s_tmp & aj_char_tmp : aj_in_escape = True
					Case """" : aj_s_tmp = aj_s_tmp & aj_char_tmp : aj_in_string = Not aj_in_string
					Case "{", "["
						aj_s_tmp = aj_s_tmp & aj_char_tmp & aj_InlineIf(aj_in_string, "", Chr(13) & Chr(10))
					Case "}", "]"
						aj_s_tmp = aj_s_tmp & aj_InlineIf(aj_in_string, "", Chr(13) & Chr(10)) & aj_char_tmp
					Case "," : aj_s_tmp = aj_s_tmp & aj_char_tmp & aj_InlineIf(aj_in_string, "", Chr(13) & Chr(10))
					Case Else : aj_s_tmp = aj_s_tmp & aj_char_tmp
				End Select
			End If

		CleanUpJSONstring = ""
		aj_s_tmp = split(aj_s_tmp, Chr(13) & Chr(10))
		For Each aj_line_tmp In aj_s_tmp
			aj_line_tmp = replace(replace(aj_line_tmp, chr(10), ""), chr(13), "")
			CleanUpJSONstring = CleanUpJSONstring & aj_Trim(aj_line_tmp) & Chr(13) & Chr(10)
	End Function

	Private Function getJSONValue(ByVal val)
		val = Trim(val)
		If Left(val,1) = ":"  Then val = Mid(val, 2)
		If Right(val,1) = "," Then val = Left(val, Len(val) - 1)
		val = Trim(val)

		Select Case val
			Case "true"  : getJSONValue = True
			Case "false" : getJSONValue = False
			Case "null" : getJSONValue = Null
			Case Else
				If (Instr(val, """") = 0) Then
					If IsNumeric(val) Then
						getJSONValue = CDbl(val)
						getJSONValue = val
					End If
					If Left(val,1) = """" Then val = Mid(val, 2)
					If Right(val,1) = """" Then val = Left(val, Len(val) - 1)
					getJSONValue = aj_JSONDecode(Trim(val))
				End If
		End Select
	End Function

	Private JSONoutput_level
	Public Function JSONoutput()
		dim wrap_dicttype, aj_label
		JSONoutput_level = 1
		wrap_dicttype = "[]"
		For Each aj_label In data
			 If Not aj_IsInt(aj_label) Then wrap_dicttype = "{}"
		JSONoutput = Left(wrap_dicttype, 1) & Chr(13) & Chr(10) & GetDict(data) & Right(wrap_dicttype, 1)
	End Function

	Private Function GetDict(objDict)
		dim aj_item, aj_keyvals, aj_label, aj_dicttype
		For Each aj_item In objDict
			Select Case TypeName(objDict.Item(aj_item))
				Case "Dictionary"
					GetDict = GetDict & Space(JSONoutput_level * 4)
					aj_dicttype = "[]"
					For Each aj_label In objDict.Item(aj_item).Keys
						 If Not aj_IsInt(aj_label) Then aj_dicttype = "{}"
					If aj_IsInt(aj_item) Then
						GetDict = GetDict & (Left(aj_dicttype,1) & Chr(13) & Chr(10))
						GetDict = GetDict & ("""" & aj_JSONEncode(aj_item) & """" & ": " & Left(aj_dicttype,1) & Chr(13) & Chr(10))
					End If
					JSONoutput_level = JSONoutput_level + 1
					aj_keyvals = objDict.Keys
					GetDict = GetDict & (GetSubDict(objDict.Item(aj_item)) & Space(JSONoutput_level * 4) & Right(aj_dicttype,1) & aj_InlineIf(aj_item = aj_keyvals(objDict.Count - 1),"" , ",") & Chr(13) & Chr(10))
				Case Else
					aj_keyvals =  objDict.Keys
					GetDict = GetDict & (Space(JSONoutput_level * 4) & aj_InlineIf(aj_IsInt(aj_item), "", """" & aj_JSONEncode(aj_item) & """: ") & WriteValue(objDict.Item(aj_item)) & aj_InlineIf(aj_item = aj_keyvals(objDict.Count - 1),"" , ",") & Chr(13) & Chr(10))
			End Select
	End Function

	Private Function aj_IsInt(val)
		aj_IsInt = (TypeName(val) = "Integer" Or TypeName(val) = "Long")
	End Function

	Private Function GetSubDict(objSubDict)
		GetSubDict = GetDict(objSubDict)
		JSONoutput_level= JSONoutput_level -1
	End Function

	Private Function WriteValue(ByVal val)
		Select Case TypeName(val)
			Case "Double", "Integer", "Long": WriteValue = replace(val, ",", ".")
			Case "Null"						: WriteValue = "null"
			Case "Boolean"					: WriteValue = aj_InlineIf(val, "true", "false")
			Case Else						: WriteValue = """" & aj_JSONEncode(val) & """"
		End Select
	End Function

	Private Function aj_JSONEncode(ByVal val)
		val = Replace(val, "\", "\\")
		val = Replace(val, """", "\""")
		'val = Replace(val, "/", "\/")
		val = Replace(val, Chr(8), "\b")
		val = Replace(val, Chr(12), "\f")
		val = Replace(val, Chr(10), "\n")
		val = Replace(val, Chr(13), "\r")
		val = Replace(val, Chr(9), "\t")
		aj_JSONEncode = Trim(val)
	End Function

	Private Function aj_JSONDecode(ByVal val)
		val = Replace(val, "\""", """")
		val = Replace(val, "\\", "\")
		val = Replace(val, "\/", "/")
		val = Replace(val, "\b", Chr(8))
		val = Replace(val, "\f", Chr(12))
		val = Replace(val, "\n", Chr(10))
		val = Replace(val, "\r", Chr(13))
		val = Replace(val, "\t", Chr(9))
		aj_JSONDecode = Trim(val)
	End Function

	Private Function aj_InlineIf(condition, returntrue, returnfalse)
		If condition Then aj_InlineIf = returntrue Else aj_InlineIf = returnfalse
	End Function

	Private Function aj_Strip(ByVal val, stripper)
		If Left(val, 1) = stripper Then val = Mid(val, 2)
		If Right(val, 1) = stripper Then val = Left(val, Len(val) - 1)
		aj_Strip = val
	End Function

	Private Function aj_MultilineTrim(TextData)
		aj_MultilineTrim = aj_RegExp.Replace(TextData, "$1")
	End Function

	private function aj_Trim(val)
		aj_Trim = Trim(val)
		Do While Left(aj_Trim, 1) = Chr(9) : aj_Trim = Mid(aj_Trim, 2) : Loop
		Do While Right(aj_Trim, 1) = Chr(9) : aj_Trim = Left(aj_Trim, Len(aj_Trim) - 1) : Loop
		aj_Trim = Trim(aj_Trim)
	end function
End Class


Key Vault, App Gateway and the KV Firewall...

If you're in a situation where you're using App Gateway along with a Key Vault certificate for SSL termination, then you cannot use the KV firewall.  We've seen a number of support cases on this issue and a Github issue explains the problems.

The use of private endpoints looks like an expected solution, but because the endpoints use private IP addresses (10.x.x.x), and the KV firewall does not allow private IP addresses through, there is a conflict between the two and it simply does not work.

This scenario was confirmed as unsupported at this time by both the App Gateway and Key Vault product teams. The official communication from the PG is this:

"When using Key Vault with Application Gateway, customers will need to select "Public endpoint (all networks)" when configuring the networking section on Key Vault. Application Gateway currently does not support integration with Key Vault if Key Vault is not configured to allow "Public endpoints (all networks)" access. We are currently working internally with the necessary teams to support all networking configurations on Key Vault with regards to integrating with Application Gateway."

Official documentation is forthcoming.

Key Vault Client: Why am I seeing HTTP 401?


A common concern with our Key Vault customers is the occurrence of an HTTP 401 (unauthorized) response from the Key Vault.  They are understandably troubled that a malicious attack on the Key Vault could be taking place, and they have alerts in place to notify them of any such responses.  However, it's not necessarily a something that we should be worried about.

What is an HTTP 401?

An overview of HTTP 401 is in order.  Amongst the set of HTTP response status codes, the 400-499 range is set aside for informing the client that there was something wrong or incorrect with the request, to the effect that an authorized valid response could not be returned. There are many response codes available, here are a couple of others:

  • 400 - Bad request.  Something was badly formatted and the server doesn't understand what the client was trying to accomplish

  • 403 - Forbidden. The client sent a properly formatted request, along with credentials that are recognized.  However, even though the client was recognized and may have permission to get some resources, this specific resource is off-limits to the client.  It's an "access denied".

As for the 401, it means "unauthorized".  The client sent a properly formatted request, but the credentials do not allow the request to get the information requested.  For Key Vault, this can be due to at least a couple of reasons:

  • Lack of an access token -  Key Vault uses Azure AAD OAUTH2 authentication.  For a Key Vault to be properly accessed, the AAD OAUTH server must issue an access token to the client, and the client must send this access token with every request to the Key Vault.  If the access token is not present, this will cause the Key Vault to reject the request with 401.

  • Bad token - If there's something wrong with the token, such as:
    • Expired
    • Not yet valid (future use token)
    • Incorrect resource.  OAUTH requires knowing the resource that you want to access in order to issue to the token.  This is a URL, and for all public key vaults, the resource is (see my previous blog post for additional information).  If this resource is not exactly correct, we get 401

You may be wondering the difference between a 401 and 403.  Let's think of a Key Vault as a house that has a security guard blocking the entrance.  Inside the house are a bunch of locked rooms containing secrets, keys and certificates.  If you attempt to go into the house, the security guard tries to figure out who you are.  If the guard cannot, then you are rejected with 401 (unauthorized).  However, you are recognized and can get in, so the guard gives you a set of keys to the rooms, but perhaps not all of the rooms. For every room that you can't get into (but try anyway with the wrong key), you get a 403 (forbidden).  For rooms where you are able to get into successfully, you get a 200 (success).

The Key Vault APIs and 401s

What I am about to describe occurs with the .NET API set, and may also occur with Node.JS, Java, and other APIs.  It does not occur with REST requests.  This will become more clear as I describe the processes.  Let's look at how to use REST first:


When developers use REST directly, they can control the exact set of commands sent to the Key Vault as well as the parameters. there is a very straight-forward series of events.  You can see actual requests/responses in my previous post:

  1. Request an access token from the AAD OAUTH services using the client id and client secret
  2. Receive a response containing an Access Token
  3. Request an object from the Key Vault.  The request contains the Access Token received in step 2.
  4. Receive a response containing the Key Vault object.

If the developer were to want a second object from the Key Vault, only steps 3 and 4 need to be repeated, unless the token is expired.  When the token expires, steps 1 and 2 must be performed again until a new Access Token is obtained.


The API sets prevent the need for the developers to manually handle the HTTP requests and parse the responses.  This makes development much faster and easier to think about.  However, the series of events is slightly different.  To show this, I've written a simple Windows form application that is used to make a request for a secret using the Key Vault .NET API set:

 public partial class Form1 : Form
        public Form1()

        string clientId = "<removed>";
        string clientSecret = "<removed>";
        KeyVaultClient keyVaultClient;
        private void Button1_Click(object sender, EventArgs e)
            Debug.WriteLine("Button1_Click started");
            keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
            Debug.WriteLine("Button1_Click ended");


        public static async Task<string> GetToken(string authority, string resource, string scope)
            Debug.WriteLine("GetToken started");
            var authContext = new AuthenticationContext(authority);
          ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
            AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
            Debug.WriteLine("Access Token is: " + result.AccessToken);
            Debug.WriteLine("GetToken ended");
            return result.AccessToken;

        private async void Button2_Click(object sender, EventArgs e)
            Debug.WriteLine("Button2_Click started");
            var Secret = await keyVaultClient.GetSecretAsync("");
            Debug.WriteLine("Secret Value at " + DateTime.Now.ToString() + ": " + Secret.Value);
            Debug.WriteLine("Button2_Click ended");

This is what it looks like when I run it:

The two buttons are clearly marked as to what they do.  However, it's when they work is the interesting part.  This is the output window from the code execution when clicking both buttons, first the "Create Key Vault Client", and secondly, "Call Key Vault":

Button1_Click started
Button1_Click ended
Button2_Click started
GetToken started
Access Token is: eyJ0eXAiOiJKV... <removed>
GetToken ended
Secret Value at 8/15/2019 3:22:39 PM: my password
Button2_Click ended

Here's the Fiddler trace from the same:


  • When "Create Key Vault Client" button is clicked, the "Button1_Click" method is executed. This means that the keyVaultClient object is instantiated, but that's it.  Even though we are passing the GetToken method into the AuthenticationCallback delegate, it does not get executed at this time.

  • However, when the "Call Key Vault" button is clicked for the first time, the following takes place in the Button2_Click method:
    • Client makes an REST call to the Key Vault to retrieve the secret, but without an access token.  This results in HTTP 401.  
    • Client then invokes the GetToken method to make a REST call to the AAD OAUTH servers to get an access token.
    • Client makes a second REST call to the Key Vault to retrieve the secret, but has the token this time - it works!

Now let's examine what happens what we see when we click the "Call Key Vault" button a second time:

Button1_Click started
Button1_Click ended
Button2_Click started
GetToken started
Access Token is: eyJ0eXAiOiJKV... <removed>
GetToken ended
Secret Value at 8/15/2019 3:22:39 PM: my password
Button2_Click ended
Button2_Click started
GetToken started
Access Token is: eyJ0eXAiOiJKV... <removed>
GetToken ended
Secret Value at 8/15/2019 3:35:49 PM: my password
Button2_Click ended

So... that's interesting.  We still executed the GetToken method...  but what does Fiddler show us?

There's only one additional HTTP request made to Key Vault, and it was successful!  This makes sense: since we already have an Access Token, there's no need to reach out to the the AAD servers to get one.  

Deep Dive
At this point I you may be trying to figure out why the Key Vault client does not get the token before making the first attempt to the Key Vault object.  Let's recall that this is a managed process meant to not only to abstract the REST methods away, but perform the operation for three circumstances:

  1. The client has no Access Token (a new Key Vault Client instance)
  2. The client has a valid Access Token
  3. The client has an expired Access Token (there is a one-hour expiration on the Access Token)

This is what's going on under the covers for KeyVaultClients objects:

  1. The Key Vault Client calls a ProcessHttpRequestAsync method to assign the current Access Token to the call to Key Vault.
  2. ProcessHttpRequestAsync calls PreAuthenticate to see if it already has a HttpBearerChallenge object (containing the Authorization URI, Resource URI, and Scope (which is not used in this case)):
    1. If the HttpBearerChallenge is null, null is returned
    2. If the Challenge information exists, then the delegate passed into AuthenticationCallback is called (GetToken) using the HttpBearerChallenge information.  GetToken fetches the Token from AuthenticationContext.AcquireTokenAsync and returns it to the ProcessHttpRequestAsync method. Note that AuthenticationContext.AcquireTokenAsync may have a cached Access Token and may not call out to the AAD Auth servers.
  3. If the Access Token is returned to ProcessHttpRequest, then the ProcessHttpRequestAsync returns the Access Token to the Operation call (GetSecretWithHttpMessagesAsync in our case) and we process the rest of the call to the Key Vault using the Access Token.  This results in a single outbound call, only to Key Vault.
  4. However, if the Access Token is not returned, ProcessHttpRequestAsync makes a call to Key Vault without a token. The response is expectedly HTTP 401.  The reason for this is to understand the HttpBearerChallenge information.  We can see this here:

  5. At this time, a call to PostAuthenticate is made in order to parse the HttpBearerChallenge information from the response, and then also calls into the AuthenticationCallback delegate (GetToken).  The delegate calls AuthenticationContext.AcquireTokenAsync.  Since there's not been an Access Token so far, the AuthenticationContext.AcquireTokenAsync method calls into the AAD Auth servers to fetch the token and return it to the PostAuthenticate method, which then returns it to the ProcessHttpRequestAsync method, and finally the Access Token is added to the HttpRequestMessage passed into the ProcessHttpRequestAsync method.

That was kind of wordy, but there are a lot of Key Vault users who want to understand this stuff. Here's the breakdown:

  1. If we don't have an Access Token, we need to make these calls:
    1. Key Vault without an Access Token to get the Authorization URI and Resource URI
    2. AAD OAUTH servers to get an Access Token
    3. Key Vault with an Access Token to get the secret (or whatever you want to do)
  2. If we have an Access Token, we need to only call the Key Vault. 
  3. If our Access Token is expired:
    1. AAD OAuth Servers to get a new Access Token
    2. Key Vault with the new Access Token to get the secret.

The main issue most seem to have a hard time with is the fact that the initial failed call is to get additional information which is required to make the subsequent call to the OAUTH services for a token.  


You can see the code for this using these links:


I hope this post helps you understand why we see the HTTP 401 on occasion.  Please respond to this post and tweet about it if you find it helpful.

Please follow us on Twitter and retweet!
@WinDevMatt @AzIdentity

This blog will have a slight change in scope

This blog was originally intended to be a place for my previous support team (the Azure Identity Incubation Team) to post content related to the various technologies that we worked on.  It turned out to be a place where I have been posting about Azure Key Vault for the past year.  To reflect this, I am changing the focus of this blog to be exclusively Key Vault.  I will be leaving the existing non-Key Vault posts in place until a better location is found for them.  Thank you.


Getting It Right: Key Vault Access Policies


Azure customers of all sizes are using ARM templates, Powershell, and CLI in order to create Registered Applications/Service Principals and then assign them to an Access Policy in the Key Vault in order to perform operations.  In performing these assignments, it is frequent that customers call into Key Vault support trying to understand why their Service Principal is receiving an HTTP 403 (Forbidden) when attempting to access the Key Vault data plane.  I want to clear up what's going on with this so that you have a better understanding and can avoid this problem.

Key Vault Access Policy In the Portal

Let's review a Registered Application that I've newly created using the portal.  We can clearly see all of the GUIDs associated with this Registered Application which are important to creating the Access Policy.

So now I create an Access Policy for this Registered Application in my Key Vault:


Next, let's get an access token using the Application ID (Client Id) and Client Secret (Key) that I created in the Application Registration blade in AAD:


Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: 168
Expect: 100-continue<removed>&grant_type=client_credentials


Response (some info removed)

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 17 May 2019 14:29:02 GMT
Content-Length: 1324


Now that I've got the token, I can make a successful request to Key Vault for a secret:


x-ms-client-request-id: 7b431436-fb67-4f44-b7c3-b91822f5d0aa
accept-language: en-US
Authorization: Bearer <removed>
User-Agent: FxVersion/4.7.3324.0 OSName/Windows10Enterprise OSVersion/6.3.17763 Microsoft.Azure.KeyVault.KeyVaultClient/

Response (some info removed)

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Expires: -1
Date: Fri, 17 May 2019 14:55:24 GMT
Content-Length: 223


You may be looking at this and wondering why I am walking you through a standard setup and request/response.  It's because I want to show you that this works as expected, even though the next step is the confusing part - the values that are kept in the Key Vault for an access policy. 

Key Vault Properties

Now look at the Key Vault using Powershell:

And more specifically, check out the Access Policy for the AKVAccessPolicyServicePrincipal:

There are three important things to note here:

  • There is no Application Id listed as a property of this Access Policy
  • The ObjectID listed here (92ba...) does not match the ObjectId (7da4...) shown in the registered application (see above)
  • The Application Id (70a3...) of the registered application from above is shown in the Display Name section

At this point you should be wondering what is going on with this, so let's figure out where the ObjectID 92ba... came from.

Enterprise Applications

Go back into the Azure portal, AAD Service, then into the "Enterprise Registrations" (have you ever even looked here before?):



Now we see that this Enterprise Application - with the same name and Application Id as my registered application - but a different ObjectID - is what we've registered in the Key Vault Access Policy!  But what in the world is an Enterprise Application?

Here are a couple of important definitions from official documentation:

  • Application objects - Although there are exceptions, application objects can be considered the definition of an application.
  • Service principals - Can be considered an instance of an application. Service principals generally reference an application object, and one application object can be referenced by multiple service principals across directories.

One additional really important piece of information from the above link:
You can manage service principals in the Azure portal through the Enterprise Applications experience. 

Now we understand - a Service Principal is NOT the same as a Registered Application and for Key Vault, we do not give an access policy to a Registered Application but to a Service Principal related to the Registered Application.  


Key Vault Access Policy via Powershell

Let's get back to Powershell and properly creating Access Policies.  I am going to delete the above Access Policy and attempt to recreate it using Powershell. There are three ways of creating an Access Policy:

  1. Set-AzureRmKeyVaultAccessPolicy [-VaultName] <String> [-ApplicationId <Guid>] -ObjectId <String> -PermissionsToKeys all
  2. Set-AzureRmKeyVaultAccessPolicy [-VaultName] <String> -PermissionsToKeys all -ServicePrincipalName <String>
  3. Set-AzureRmKeyVaultAccessPolicy [-VaultName] <String> -PermissionsToKeys all -UserPrincipalName <String>

For most people with an Application Id and Object Id, #1 might seem to do the trick, so let's see what happens when we use this.  Let's also have a reminder of what it looks like when properly created in the portal:

Upon running this cmdlet, we are immediately met with a request for an ObjectId.  I can put in the ObjectId of the Registered Application and it seems fine until I try to use it (I won't show that).  Let's see what that looks like in the Key Vault:

So that really might look ok to most people, but as you noticed above, that's not what we really want.  The Object Id is not correct (we know that now), and the correct working Access Policy does not have an Application Id but this one does.  I reminded you what a properly registered Access Policies looks like, so let's look at the one we just created:

You can see that the name is correct, but the icon and coloring is different.  This should be a clue that something is wrong.  Let's look at doing this correctly in one of two ways:

  1. Use the Object ID of the Enterprise App.  This is identical to the Access Policy we created earlier in the portal, and the icon looks correct:

  2. Use the Application Id of the Registered Application as the Service Principal name. This automatically extracts the Enterprise Application Object ID and places it into Object ID of the Key Vault properties, and also populates the Display Name - exactly like above.


Application Id

At this point you are probably asking what an Application Id is for in an Access Policy.  Go back to the portal and into the new Access Policy blade.  Notice the "Authorized Application" part at the bottom of this image:

The Application Id property of a Key Vault refers directly to that "Authorized Application" part of an Access Policy. This is for On-Behalf-Of Authorization scenarios which means that authorization is granted to a specific user only via a specific application. Without other Access Policies, the user cannot access the Key Vault without the app, and the app cannot access the Key Vault without the user.  Most organizations will not use this feature, but I know that some have. 


It's important to know exactly the correct Object ID to use in an Access Policy.  The difference between a registered application and an Enterprise App/Service Principal seems to be the #1 most confusing part of this.  However, I hope this blog post clears up the issues around this and makes your life easier when troubleshooting your Access Policy issues.

Finally: I want to thank Scott Cottrille, Principal Group Software Engineering Manager of Azure Key Vault for the inspiration for this post.

I hope this helps you out when using the Azure Key Vault!  Please follow us on Twitter and retweet!
@WinDevMatt @AzIdentity


Azure Key Vault Documentation "Try It" Pages

A number of customers call into Microsoft Support when they go to the documentation for Azure Key Vault REST API and use the "Try It" pages.  This is one of those pages:


They go through the process of putting in the correct information only to find that they get a 401 or a 404 or some error saying that it's not working.  Their real concern is that  they do not understand what's going on, they've done something wrong and when they go to implement code to call Key Vault in production, it's not going to work.  Unfortunately, it's the "Try It" page that is not working correctly, and this is why:

The "Try It" test harness is built using Swagger. These "Try It" pages are automatically generated, and given that Azure REST APIs mainly work with ARM, the resource that Swagger uses to generate is always  However, as I've previously described, the correct resource to use is (no slash!).  Let's look at the resource (also known as the audience) of the token generated for the above request: