Implementing a Token Store with APIM Authorizations

Aaron Powell - Jun 8 '22 - - Dev Community

In this post, we're going to take a look at the recently previewed Authorizations feature of Azure API Management (APIM) and see how to setup a React and TypeScript application that uses the Dropbox SDK to upload a file, without needing to handle OAuth token creation.

What is APIM Authorizations

Before we dive into creating the application, let's quickly look at what this feature is.

In a connected system, being able to communicate between different Software as a Service (SaaS) platforms is a common task, but often these platforms will use OAuth2 to verify the user identity. This requires undertaking an authentication flow, which is fine if you're directly using the system, but what if it's being handled by a background job, like an Azure Function running with a Timer Trigger? Then we need to use alternative authentication workflows, handle expiry of tokens, etc.

This can result in a lot of our application code being responsible for managing and storing tokens.

And this is where Authorizations comes in, it is a managed Token Store for your OAuth2 access tokens. Rather than your application having to authenticate, APIM will handle this on your behalf. It also means your application can operate in a lower trust environment, rather than your application needing to know about the client id/client secret of the SaaS provider, it becomes unaware and only relies on the REST API to API Management to get the token back as-needed.

You can learn more about Authorizations in APIM on their docs.

Creating our app

The application we're creating is a data entry form that could be used to capture user information while at an event, a person will enter their information and it'll generate a file to upload to Dropbox, which could later be ingested by another part of our system.

Let's start by generating the new application using vite:

npm create vite@latest my-app -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

Next, we'll start creating the form that we'll use for data capture, so open the my-app folder in VS Code (or any other editor of your choice) and we'll add a form to the App.tsx file:

const updateField =
  (updater: React.Dispatch<React.SetStateAction<UserInfo>>) =>
  (e: ChangeEvent<HTMLInputElement>) =>
    updater((userInfo) => ({
      ...userInfo,
      [e.target.name]: e.target.value,
    }));

function App() {
  const [userInfo, setUserInfo] = useState<UserInfo>({});
  const [submitting, setSubmitting] = useState(false);

  return (
    <div className="App">
      <header className="App-header">
        <h1>Contoso Lead Capture</h1>
        <form
          action=""
          onSubmit={(e) => (e.preventDefault(), setSubmitting(true))}
        >
          <fieldset>
            <div>
              <label htmlFor="firstName">First name</label>
              <input
                type="text"
                name="firstName"
                id="firstName"
                placeholder="Aaron"
                value={userInfo.firstName}
                onChange={updateField(setUserInfo)}
              />
            </div>
            <div>
              <label htmlFor="lastName">Last name</label>
              <input
                type="text"
                name="lastName"
                id="lastName"
                placeholder="Powell"
                value={userInfo.lastName}
                onChange={updateField(setUserInfo)}
              />
            </div>
          </fieldset>

          <fieldset>
            <div>
              <label htmlFor="email">Email</label>
              <input
                type="email"
                id="email"
                name="email"
                placeholder="foo@email.com"
                value={userInfo.email}
                onChange={updateField(setUserInfo)}
              />
            </div>

            <div>
              <label htmlFor="phone">Phone</label>
              <input
                type="phone"
                id="phone"
                name="phone"
                placeholder="555-555-555"
                value={userInfo.phone}
                onChange={updateField(setUserInfo)}
              />
            </div>
          </fieldset>

          <fieldset>
            <button
              type="submit"
              disabled={
                submitting ||
                !userInfo.firstName ||
                !userInfo.lastName ||
                !userInfo.email ||
                !userInfo.phone
              }
            >
              Submit
            </button>
          </fieldset>
        </form>
      </header>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

I've also brought in the useState hook so that we can set the values of the various fields as we go along and created a type the represent the data in the form (and put it in a new file called types.ts):

export type UserInfo = {
  firstName?: string;
  lastName?: string;
  email?: string;
  phone?: string;
};
Enter fullscreen mode Exit fullscreen mode

Hooking up to Dropbox

It's time to hook up with Dropbox, so we'll need their JavaScript SDK:

npm install --save dropbox
Enter fullscreen mode Exit fullscreen mode

And we'll put the save process in a useEffect hook:

  const [dropboxResponse, setDropboxResponse] = useState<
    DropboxSaveResponse | undefined
  >();

  useEffect(() => {
    async function saveToDropbox() {
      const accessToken = "???";

      const dropbox = new Dropbox({ accessToken });

      const contents = `${userInfo.firstName},${userInfo.lastName},${userInfo.email},${userInfo.phone}`;
      const path = `/submissions/${+new Date()}.csv`;

      const response = await dropbox.filesUpload({
        path,
        contents,
      });
      if (response.status !== 200) {
        setDropboxResponse({
          error: true,
          message: "Failed to upload to dropbox",
        });
        return;
      }

      setDropboxResponse({
        error: false,
        message: "Details have been saved. Start again?",
      });
    }

    if (!submitting) {
      return;
    }

    saveToDropbox();
  }, [submitting, userInfo])
Enter fullscreen mode Exit fullscreen mode

I've also created a type called DropboxSaveResponse to set on the hook:

export type DropboxSaveResponse = {
  error: boolean;
  message: string;
};
Enter fullscreen mode Exit fullscreen mode

Our code is ready, well, except for one critical part - how do we get our access token for the Dropbox SDK? Well, we could kick off a Dropbox auth flow, but now everyone has to be able to approve access to the shared Dropbox account, which isn't ideal. Thankfully, this is exactly what APIM Authorizations is designed for.

Setting up APIM with Authorizations

We're going to use the Azure Portal to deploy our APIM instance, but as part of the sample repo, we've also provided some Bicep templates, so if that's your preferred approach, head over to the GitHub repo for that guide. Also, if you just want to get deployed, click the Deploy to Azure button below:

Deploy To Azure

Note: Please be aware this is preview so there may be some changes before the final release.

Head over the the Azure Portal and create a new APIM instance:

Create an APIM instance

Fill in the required fields and click through the other screens (there's nothing more that we need to add to the APIM resource beyond the first screen - unless you want to configure APIM for other uses).

Note: For the preview, you'll need to use the Developer pricing tier.

When the resource has been created, you should see a new Authorizations (preview) option under the APIs grouping:

Navigate to Authorizations

Click on that and we'll see a list of previously created Authorizations, but since we haven't got any yet, we'll start with the Create button to provision it:

Authorizations landing view

From this screen, we can configure the OAuth2 service that we are going to authorize against, and you'll see all that's available in the Identity provider list. Since we're using Dropbox, you'll need to have created a Dropbox app and obtained the client id and client secret already (if you haven't done that, head over to Dropbox and set that up).

When filling out this form, note down the Provider name and Authorization name, as we're going to need those later on.

Also, ensure that the Scopes you provide match that in Dropbox. Since we're going to be uploading files we're going to need files.metadata.write files.contents.write files.content.read, but match those to your applications needs.

Create an Authorization
Before going to the next screen, copy the Redirect URL and add that to the Dropbox application, so that it can authenticate on the next step:

Authenticate the Authorization

On step two of the process, we need to authenticate APIM against our Dropbox application using the OAuth2 application we've created, so click the Login with DropBox button and follow the authorization workflow that it provides.

The last stage of setting up the Authorization is configuring the Access Policy the Authorization will use, you can either link this to users/groups within AAD or you can use a managed identity, such as the one provided by APIM. We're going to use the managed identity:

Select Managed Identity

From the fly-in window select API Management service for the Managed identity and then pick your service from the listed options.

Chose the right Managed Identity

This will populate the main window and we can finish the setup.

Created Authorization

Accessing our token

APIM is now acting as our Token Store, and will get new OAuth2 tokens for us as required, but we still need to access them, and for that, we're going to create an API endpoint in API to return it. Head over to the APIs section and we're going to manually define a HTTP API:

Define a new API

The API I've defined will be available at the /token route, and since we'll be calling it from another web host, we need to configure a CORS policy. We can do that by clicking on All operations and opening the code editor for policies to replace the default with:

<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>
Enter fullscreen mode Exit fullscreen mode

This is defining an inbound policy that allows CORS from all origins (you might want to tighten that up in a production app!) and passes through all requests to the backend without interference.

Now we can create an operation to the API so that we can get back the token:

Create an API operation

I'm calling the operation Get Dropbox token and making it a HTTP GET at the / URL, which is relative to the path of the API that we've defined, meaning it's a GET request against /token.

With that saved, we need to define just what this API will do. Since we want to access the token store that our authorizations use, we're going to use the get-authorization-context policy on the inbound request:

<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(&quot;auth-context&quot;))?.AccessToken)</set-body>
    </return-response>
</inbound>
<backend>
    <base />
</backend>
<outbound>
    <base />
</outbound>
<on-error>
    <base />
</on-error>
</policies>
Enter fullscreen mode Exit fullscreen mode

The get-authorization-context policy needs two bits of information that we set when we created the Authorization initially, the name of the provider, dropbox-demo, and the name of the Authorization, auth. The policy will then call into our token store, grab the token and we set it as the body using set-body, to return in our response. This is just setting a text/plain response, but you could build up a JSON payload if that was more preferred in your scenario.

Save the policy, click the Test tab at the top and fire off the request:

Test our API

Success! We can see in the HTTP response that the response body contains our OAuth2 token that we can can provide to the Dropbox SDK.

Hooking it all up

APIM is all configured with the authorizations now so it's time to integrate with our application.

From the React application we're going to make a call to the /token API that we created, and you can get the URL from this command:

SUBSCRIPTION_KEY=$(az rest --method post --url /$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ApiManagement/service/$APIM_NAME/subscriptions/master/listSecrets?api-version=2021-08-01 | jq .primaryKey -r)
GATEWAY_URL=$(az apim show --name $APIM_NAME --resource-group $RESOURCE_GROUP --query gatewayUrl --output tsv)

echo "$GATEWAY_URL?dropbox-demo/token?subscription-key=$SUBSCRIPTION_KEY"
Enter fullscreen mode Exit fullscreen mode

Note: We are going to be including the subscription key in the URL and that key will be exposed via the React app so it can call APIM, meaning you are potentially leaking secrets. In a more robust application you'd likely include an Azure Function which makes the call to Dropbox, rather than doing it in the browser, so your client would POST to Azure Functions and it in turn would retrieve the access token and upload the file. But we're keeping it in the client for today's demo.

To use this from our React application, create a .env file at the root of your workspace and add it in like so:

VITE_APIM_ENDPOINT=<...>
Enter fullscreen mode Exit fullscreen mode

Now we can go back to our App.tsx and update this line:

const accessToken = "???";
Enter fullscreen mode Exit fullscreen mode

To:

const accessTokenResponse = await fetch(import.meta.env.VITE_APIM_ENDPOINT);
const accessToken = await accessTokenResponse.text();
Enter fullscreen mode Exit fullscreen mode

Start the application with npm run dev, fill out the data in the form and hit submit - you'll see a call to APIM that gets back the access token and then it's provided to the Dropbox SDK to upload the file to Dropbox.

Sample app in action

Conclusion

There we have it, you've learnt about a new feature we've added to API Management - Authorizations.

Throughout this post we've taken a look at how to setup Authorizations in APIM, in this case we've used Dropbox, connected APIM to our Dropbox application to it can request OAuth2 access tokens on our behalf. We then created a policy in APIM that will return the access token via an API call we can make, rather than us having to build our own API from scratch.

We also built a React application that can call the API we created in APIM to get back the Dropbox access token from the token store, provide it to the Dropbox SDK and then upload a file to Dropbox, all without the client having to undertake an OAuth2 flow itself.

You'll find the sample of this application on GitHub, including the scripts for provisioning APIM and a Blazor/C# version. To learn more about the Blazor version, check out this article by my colleague Justin Yoo.

Don't forget to have a read of the Authorizations in API Management docs and let us know what kinds of things you would find this useful for.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player