Efficient OAuth Authorisation Management in Azure API Management

An auth key stored in Key Vault is a simple problem to solve, fetch it, use it, done. OAuth is a different story entirely. The moment your app needs to call a downstream API that only accepts OAuth access tokens, you are signing up for the full authorization code dance: requesting a code, exchanging it for an access token and refresh token, calling the API, catching the eventual expiry, and repeating the refresh exchange indefinitely.

The full OAuth authorization code flow an application would otherwise have to implement itself.
The full OAuth authorization code flow an application would otherwise have to implement itself.

Every one of those steps is boilerplate you have written before if you have integrated more than one OAuth provider, and every provider has its own quirks in how it expects that boilerplate implemented. Azure API Management’s Authorizations feature, in preview at the time of writing, takes that entire dance and runs it on your behalf, handing your application a plain access token whenever it asks.

What the Client Application Actually Sees

From the calling application’s side, there is no OAuth code left to write at all. A Blazor WASM app, for instance, just calls a plain APIM endpoint and gets a token string back.

private async Task SaveToDropboxAsync()
{
    // Gets the APIM endpoint from appsettings.json
    var requestUrl = Configuration.GetValue<string>("APIM_Endpoint");
 
    // Gets the auth token from APIM
    var token = await Http.GetStringAsync(requestUrl).ConfigureAwait(false);
 
    // Builds contents.
    var path = $"/submissions/{DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmss")}.csv";
    var contents = $"{userInfo.FirstName},{userInfo.LastName},{userInfo.Email},{userInfo.Phone}";
    var bytes = UTF8Encoding.UTF8.GetBytes(contents);
 
    // Uploads the contents.
    var result = default(FileMetadata);
    using (var dropbox = new DropboxClient(token))
    using (var stream = new MemoryStream(bytes))
    {
        result = await dropbox.Files.UploadAsync(path, WriteMode.Overwrite.Instance, body: stream).ConfigureAwait(false);
    }
}

Notice there is no client ID, no client secret, no token cache, and no refresh logic anywhere in this method. Http.GetStringAsync hits an APIM endpoint that happens to return a raw DropBox access token as its response body, ready to hand straight to the DropBox SDK. The APIM_Endpoint value itself lives in a plain appsettings.json entry alongside the APIM subscription key.

{
  "APIM_Endpoint": "https://<APIM_NAME>.azure-api.net/dropbox-demo/token?subscription-key=<APIM_SUBSCRIPTION_KEY>"
}

How the APIM Side Is Actually Wired Up

All the real work happens in Bicep defining the APIM instance and its policies, so this is worth walking through piece by piece rather than treating it as a black box. First, the APIM instance itself, with Managed Identity enabled, since that identity is what APIM eventually uses to authenticate itself to the downstream OAuth provider.

// APIM instance
resource apim 'Microsoft.ApiManagement/service@2021-08-01' = {
  name: 'token-store-demo-apim'
  location: 'westcentralus'
  sku: {
    name: 'Developer'
    capacity: 1
  }
  properties: {
    publisherName: 'John Doe'
    publisherEmail: 'john.doe@nomail.com'
  }
  identity: {
    type: 'SystemAssigned'
  }
}

Because a browser-hosted Static Web App calls this APIM endpoint directly, cross-origin requests need to be allowed at the service level. The origin wildcard below is fine for a demo, but tighten it to your actual app’s URL before this goes anywhere near production, an open CORS policy on an endpoint that hands out access tokens is not something to leave loose.

// Service Policy
resource apim_policy 'Microsoft.ApiManagement/service/policies@2021-08-01' = {
  parent: apim
  name: 'policy'
  properties: {
    value: service_policy
    format: 'xml'
  }
}
 
// Service Policy Definition
var service_policy = '''
<policies>
    <inbound>
        <cors allow-credentials="false">
            <allowed-origins>
                <origin>*</origin>
            </allowed-origins>
            <allowed-methods>
                <method>GET</method>
                <method>POST</method>
            </allowed-methods>
        </cors>
    </inbound>
    <backend>
        <forward-request />
    </backend>
    <outbound />
    <on-error />
</policies>'''

Next, the API and the specific operation that will hand back the token. Note that DropBox’s real API does not have a /token endpoint, this is a virtual operation APIM exposes purely as a front for the authorization feature, it never actually forwards a request to DropBox itself.

// API
resource api 'Microsoft.ApiManagement/service/apis@2021-08-01' = {
  name: 'dropbox-demo'
  parent: apim
  properties: {
    serviceUrl: 'https://api.dropboxapi.com'
    path: 'dropbox-demo'
    displayName: 'dropbox-demo'
    protocols: [
      'https'
    ]
  }
}
 
// Operation
resource api_gettoken 'Microsoft.ApiManagement/service/apis/operations@2021-08-01' = {
  name: 'gettoken'
  parent: api
  properties: {
    method: 'GET'
    urlTemplate: '/token'
    displayName: 'gettoken'
  }
}

The operation policy is where the actual mechanism lives. get-authorization-context is the node doing the work: it looks up a previously configured authorization by provider-id and authorization-id, uses the APIM instance’s managed identity to fetch or refresh the token behind the scenes, and stores the result in the named context variable. return-response then short-circuits the request entirely and hands the caller the access token directly, instead of forwarding anything to a real backend.

// Operation Token Policy Definition
var operation_token_policy = '''
<policies>
    <inbound>
        <base />
        <get-authorization-context provider-id="dropbox-demo" authorization-id="auth" context-variable-name="auth-context" ignore-error="false" identity-type="managed" />
        <return-response>
            <set-body>@(((Authorization)context.Variables.GetValueOrDefault("auth-context"))?.AccessToken)</set-body>
        </return-response>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>'''

identity-type set to managed is the piece that ties back to the SystemAssigned identity on the APIM resource earlier. Without that identity, APIM would have no credential of its own to authenticate against the OAuth provider on your behalf, and this policy would fail at runtime rather than at deployment time, which makes it an easy thing to miss until you actually hit the endpoint.

The Part That Cannot Be Done Purely in Bicep

Deploying the Bicep templates gets you the plumbing, but the actual OAuth consent still has to happen once, interactively, through the Azure portal. In the APIM instance, under Authorizations, you create an authorization entry that matches the provider-id and authorization-id used in the policy, supplying the OAuth provider’s client ID, client secret, and requested scopes.

Creating an authorization entry in APIM with the provider's client ID, secret, and scopes.
Creating an authorization entry in APIM with the provider’s client ID, secret, and scopes.

Creating this entry produces a redirect URL that needs to be registered on the OAuth provider’s side, DropBox in this walkthrough, after which you log into the provider once through APIM’s own consent flow. That one-time interactive step is what actually produces the initial access and refresh tokens that APIM will keep renewing afterward.

There is a second, easy to miss authorization step left after that: the DropBox app being authorized is not the same thing as APIM’s managed identity being allowed to use that authorization. You still need to explicitly add the APIM instance’s managed identity as a member on the authorization resource.

Granting the APIM managed identity access to the authorization resource.
Granting the APIM managed identity access to the authorization resource.

Skip this step and the operation policy will fail at request time even though the OAuth consent itself succeeded, since APIM has no way to prove it is allowed to read the stored tokens without that membership in place.

What This Buys You, and Its Limits

The genuine win here is refresh handling. Once the consent step is done, APIM keeps the access token current on your behalf indefinitely, your application only ever asks for a token and gets a valid one back, with no refresh logic, no expiry tracking, and no token cache to build and secure yourself. That is a real chunk of work removed from every application that would otherwise need to talk OAuth to a downstream provider.

Being a preview feature at the time this was written is the honest caveat: expect rough edges, and check current documentation before betting production traffic on it. It is also worth being clear about what this replaces and what it does not. This is solving credential lifecycle management for OAuth specifically, it is not a general purpose secrets store, and for auth keys or connection strings that do not need a refresh cycle, Key Vault remains the simpler and more appropriate tool.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading