Blog Post

Apps on Azure Blog
9 MIN READ

Use Static Web Apps API and API Management Authorizations to integrate third party services

anninakeller's avatar
anninakeller
Icon for Microsoft rankMicrosoft
Sep 08, 2022

 

This article is an addition to New API backend options in Azure Static Web Apps

 

Introduction to Azure API Management

APIs enable digital experiences, simplify application integration, underpin new digital products, and make data and services reusable and universally accessible. With the proliferation and increasing dependency on APIs, organizations need to manage them as first-class assets throughout their lifecycle. Azure API Management helps customers meet these challenges:

  • Abstract backend architecture diversity and complexity from API consumers
  • Securely expose services hosted on and outside of Azure as APIs
  • Protect, accelerate, and observe APIs
  • Enable API discovery and consumption by internal and external users

 

APIM Authorization

API Management Authorizations greatly simplifies the process of authenticating and authorizing users across one (or) more SaaS services. It reduces the development cost in ramping up, implementing and maintaining security features with service integrations.

It lets you configure OAuth, Consent, Acquire Tokens, Cache Tokens and refreshes tokens for multiple services without writing a single line of code. API Management does all the heavy lifting for you, while you can focus on the application/domain logic.

 

Walk Through ā€” Azure Static Web Apps integration with Azure API Management

Imagine having a GitHub repository to which you collect issues from your customers. Now, to post a GitHub issue, the customer requires a GitHub account. However, some customers may not have a GitHub account, preferring to use their Azure Active Directory to create a new issue. Let's create an app for this scenario that uses a GitHub OAuth app created for these customers to post the issues and adds additional information to the ticket indicating which AAD user created the issue.

 

GitHub

First, we will need to create a new GitHub repository which will be the one we will be writing issues to. You can also simply use the repository you will anyways need to create a static web app. Then we will need to create a new GitHub OAuth App using GitHub: Profile Icon > Settings > Developer Settings > OAuth Apps > New OAuth App. You can start by using placeholder values for the Homepage and Callback URL. Make sure the app has access to write to repositories and allow it to create issues by setting Issues to Read and Write.

ā€ƒ

API Management

Since we will need to use a GitHub OAuth app and do not want to take care of any token storing or refreshing, we can simply use the new Authorizations feature provided by Azure API Management. To start off we will need to create a new API Management instance (or reuse one of our existing ones). We will need APIM for this scenario since the resource will take care of signing our sent through request with the right Authorization header created using the provided OAuth app. For this to work we will need to configure the instance using APIM Authorization. This will allow us to register a GitHub OAuth account and so we will be able to post issues to GitHub using that user. You can almost follow this Tutorial to setup your APIM Instance properly. There are a few things we will need to configure differently:

  1. Instead of adding the GET /Users API we will add the POST /repos/{github-alias}/{reponame}/issues API to the APIM Instance
  2. Add the Web service URL https://api.github.com and the API URL suffix /api. The api suffix will be important later on when we link this backend to our static web app.
  3. Now you will need to add the Authorization to your API Management Instance. Make sure that the scope is repo. Learn more about GitHub Scopes following this Link.

Once you added the Authorization, we can make use of the provider in the Inbound Processing Policy of the previously added API. Add the following snipped to the inbound JWT policy:

<policies>
    <inbound>
        <base />
        <get-authorization-context provider-id="github-01" authorization-id="auth-01" context-variable-name="auth-context" identity-type="managed" ignore-error="false" />
        <set-header name="Authorization" exists-action="override">
            <value>@("token " + ((Authorization)context.Variables.GetValueOrDefault("auth-context"))?.AccessToken)</value>
        </set-header>
        <set-header name="User-Agent" exists-action="override">
            <value>API Management</value>
        </set-header>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Try out the endpoint by using the Testing tab. Send a request using the following body: {"title":"Found a bug","body":"This is a test.","labels":["bug"]}.

You can verify it worked by opening the issues tab of your previously created GitHub repo where you should then be able to see the created issue.

Static Web Apps

What we want to do now is build a front end that makes use of this API and provides some additional information about the actual end user creating the issue, since on GitHub side every issue will be created by the same account that was used to create the OAuth app. To do this we create a new static web app. You can create a static web app using this template and then deploy to a static web app from this repository. Follow this tutorial to learn how to do that. Then we will create a simple from which is going to look like this:

 

The Post to GitHub submit button will do a simple POST to the API Management instance to our issues endpoint. You can copy the following code. We are adding one condition which checks if the user is logged out and if they are not a login button will be shown that simply forwards to the /.auth/login/aad endpoint which will handle the login part for us. Learn more about the out of the box Authentication and authorization for Azure Static Web Apps.

 

Form.js

import React from "react";
import axios from "axios";
import { useState } from "react";
import { useEffect } from "react";

const Form = () => {
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");
  const [sent, setSent] = useState(false);
  const [loggedIn, setLoggedIn] = useState(false);

  useEffect(() => {
    const fetchCurrentUser = async () => {
      const response = await fetch("/.auth/me");
      const payload = await response.json();
      const { clientPrincipal } = payload;
      return clientPrincipal;
    };
    fetchCurrentUser()
      .then((response) => {
        if (response !== null && response !== undefined) {
          setLoggedIn(true);
        }
      })
      .catch((error) => console.log(error));
  }, []);

  const handleSubmit = () => {
    const issue = {
      title: title,
      body: body,
    };
    axios.post("/api/issues", issue).then((res) => {
      setBody("");
      setTitle("");
      setSent(true);
    });
  };

  return (
    <div className="wrapper">
      {loggedIn ? (
        <>
          <input
            placeholder="Title"
            type="text"
            name="title"
            onChange={(event) => setTitle(event.target.value)}
            value={title}
          />
          <textarea
            placeholder="Description"
            type="text"
            name="description"
            onChange={(event) => setBody(event.target.value)}
            value={body}
          />
          <button className="blueButton" onClick={handleSubmit}>
            Post to GitHub
          </button>
          <div className="successMessage">
            {sent ? "Successfully sent to backend! <3" : ""}
          </div>
        </>
      ) : (
        <a className="blueButton" href="/.auth/login/aad">
          Login with Azure Active Directory
        </a>
      )}
    </div>
  );
};

export default Form;

 

App.js

 

import React from 'react';
import Form from './Form'

function App() {
  return <Form/>;
}

export default App;

 

 

index.css

 

* {
    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
}

.wrapper {
    min-width: 100vh;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

textarea {
    height: 200px;
    width: 151px;
}

input[type="text"], textarea[type="text"] {
    border-radius: 12px;
    border: 1px solid lightgray;
    padding: 16px 64px 16px 8px;
}

input[type="text"]:focus, textarea[type="text"]:focus {
    outline-width: 0;
    border: 1px solid gray;
}

.wrapper>* {
    margin: 10px;
}

.blueButton {
    padding: 16px 32px;
    border: none;
    border-radius: 12px;
    color: white;
    background-color: rgb(0, 138, 215);
    cursor: pointer;
    font-weight: 650;
    text-decoration: none;
    font-size: 16px;
}

.successMessage {
    font-size: small;
    color: green;
}

 

 

Afterwards you need to continue and redeploy the app to Azure Static Web Apps.

 

Linking

You can see in the Form.js file we are simply calling an /api/* endpoint that currently does not exist yet. To make the scenario work we will now need to go ahead and Link the API Management instance to our newly created static web app.

After the apps are linked, you will need to go to the API Management Instance find the Product that was created containing you Static Web Apps default hostname in the name and add the created API to the Product. Follow this documentation on how to link an API Management instance to a static web app. When you now open the static web app, login with AAD, fill out the form and hit Post to GitHub you will be able to open you GitHub's Issues page and see the issue being created.

 

Restrict API access

To allow only logged in AAD users to call our linked API we can simply extend our staticwebapp.config.json file with a routing rule like the following

 

{
  "navigationFallback": {
    "rewrite": "/index.html"
  },
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    }      
  ]
}

 

This will allow only authenticated traffic to any routes containing the api prefix.

 

Append User Data

Currently when an issue is posted it will only contain the content being sent from the backend. If we would want to make use of the X-MS-CLIENT-PRINCIPAL header which is sent from the browser if a user is logged in and allows us to identify the end user, we can simply extend our APIM JWT Inbound policy to look like this.

 

 

<policies>
    <inbound>
        <base />
        <set-variable name="SwaClientPrincipal" value="@{
            var data = context.Request.Headers.GetValueOrDefault("x-ms-client-principal", "");
            if (data == "")
            {
                return JObject.FromObject(new { userRoles = new [] { "anonymous" } });
            }
            var decoded = System.Convert.FromBase64String(data);
            var json = System.Text.Encoding.UTF8.GetString(decoded);
            return JObject.Parse(json); 
        }"/>
        <set-variable name="SwaUserDetails" value="@(string.Join(",", context.Variables.GetValueOrDefault<JObject>("SwaClientPrincipal")["userDetails"]))" />
        <get-authorization-context provider-id="github-01" authorization-id="auth-01" context-variable-name="auth-context" identity-type="managed" ignore-error="false" />
        <set-header name="Authorization" exists-action="override">
            <value>@("token " + ((Authorization)context.Variables.GetValueOrDefault("auth-context"))?.AccessToken)</value>
        </set-header>
        <set-header name="User-Agent" exists-action="override">
            <value>API Management</value>
        </set-header>
        <set-body>@{
            JObject body = context.Request.Body.As<JObject>(); 
            body["body"] = body["body"] + "\n" + "**Issue created by:** " + context.Variables.GetValueOrDefault<JObject>("SwaClientPrincipal")["userDetails"];
            var lables = new List<string>() { "aadUser", "bug" };

            foreach (var role in context.Variables.GetValueOrDefault<JObject>("SwaClientPrincipal")["userRoles"]) {
                var roleString = role.ToString();
                if (roleString != "anonymous") {
                     lables.Add(roleString);
                }
            }

            body["labels"] = JToken.FromObject(lables);
            return body.ToString(); 
        }</set-body>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

 

What we are doing now is we are reading in the X-MS-CLIENT-PRINCIPAL header, parse it and then use the contained email address to append it to the request body. In addition, we get the user role from the header which could be authenticated or anonymous and if the user is authenticated, we add it to the list of labels we are sending to GitHub. We also add an additional tag aadUser to specify that this issue was created by our app and not by the actual user. When we now again send a request using the form provided through our static web app the finally created ticket will look something like this:

ā€ƒā€ƒ

 

Here the Link to the final GitHub project: aad-issues

 

Annaji Ganti (Twitter: @annajiganti) and Annina Keller (Twitter: @anninake)

 

Updated Sep 08, 2022
Version 2.0