The cheap way to make multi tenant SaaS on Azure #1 | Poor man's Azure

Many people say Azure is expensive. But I think Azure isn't so expensive compared AWS or GCP on full workload.
However it's true that minimum cost is expensive on Azure.

Therefore I've decided to find to make multi tenancy application on Azure as cheap as possible and I'm going to try to write a series of them.

In this commemorable first article, I consider front-end and authentication.

Azure has many scenarios to host web application, such as WebApps, VM, Container Apps, aks, aci, Static WebApps(swa), Functions, etc...

But there are not many one which has free tier.

Let's check free services of Azure at this web page.

Following three services can host web service with free. Other services, such as VM, Container Apps, aks, aci, don't have free tier and aren't cheap.

  • Web Apps Free instance
    • No Custom Doamin
    • 60min/day CPU
    • 1GB Storage
  • Static Web Apps
    • 2 Custom Domain
    • 5GB Storage
  • Functions(Consumption)
    • 1M requests
    • Custom Domain

Then, which service do I select to host my app? I think swa is the best.
Because free instances of web apps cann't handle any custom domain. Functions can handle custom domain and host an HTTP trigger function to return an HTML file but it's not good DX for front-end(html&js) devlopment.

Great news of swa came last month. It supports Next.js SSR. Swa has become the perfect host service for a modern web application!

I decide to use swa to host Next.js application. Amazingly, I use it as free.

But what about for authentication, what should I use and how?

It is generally split into two choices to implement authentication for multi tenants.

One is like GitHub. There is one account store and many organizations. Users are stored in the shared account store and join to several organizations.

Another is like Slack. There are organizations which has each account store. If org-A and org-B exist and each have a same email user(mail@iwate.me), org-A's mail@iwate.me user is not equal to org-B's mail@iwate.me user.

GitHub style is easy to collaborate people across organisations. So I decide make this style.

Azure has a IDaaS, Azure AD B2C(aad b2c). It has good security features and is free until 50,000 active users per month. There is no way to not use.

But there is a problem. It's how to connect to swa.

If you have used azure app services, You think of its authentication and authorization, it was called easy-auth.

However, the feature of swa needs standard plan. So it can't use on free plan.

Omg! but it's still too early to be fall.

Swa can host SSR Next.js now. So we can use NextAuth.js! We don't need standard plan for easy-auth.

1. Create Static Web Apps instance

Create swa instance on portal and get its URL like as https://xxxx-xxxx-xxxxxxxxx.x.azurestaticapps.net

2. Create AAD B2C and its User-Flow

IMPORTANT

displayName and email is needed NEXT Auth. You must return them to app.

3. Register Application to AAD B2C

Input redirect URIs:

  • [Created SWA URL on 1.]/api/auth/callback/azure-ad-b2c
  • [Created SWA URL on 1.]/api/auth/signout
  • [endpoint for local Next.js]/api/auth/callback/azure-ad-b2c
  • [endpoint for local Next.js]/api/auth/signout

4. Create a Backend Azure Functions Instance

I've dicided to use swa for front-end. However I think it is not enough for my application. I love C# and I want to write business logic with C#. So Let's create Azure Function and host business logic api application.

Don't worry, Azure Function (Consumption) has free tier until 1M requests.

Create a comsumption function and host a http trigger, such as:

And enable easy-auth for AAD B2C.

5. Create Next.js application

Install Next.js, NextAuth.js and dependencies.

npm install next react react-dom next-auth swr

Open package.json and add the following scripts.

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint"
}

Create auth endpoint file.

// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import AADB2CProvider from "next-auth/providers/azure-ad-b2c"
export const authOptions = {
    // Configure one or more authentication providers
    providers: [
        AADB2CProvider({
            tenantId: process.env.AZURE_AD_B2C_TENANT_NAME,
            clientId: process.env.AZURE_AD_B2C_CLIENT_ID,
            clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET,
            primaryUserFlow: process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW,
            authorization: { params: { scope: "offline_access openid" } },
        })
        // ...add more providers here
    ],
    callbacks: {
        async jwt({ token, account }) {
            // Persist the OAuth access_token to the token right after signin
            if (account) {
                token.idToken = account.id_token
            }
            return token
          },
        async session({ session, token, user }) {
            // Send properties to the client, like an access_token from a provider
            session.idToken = token.idToken
            session.apiEp = process.env.NEXT_PUBLIC_API_EP
            return session;
        }
    }
}
export default NextAuth(authOptions)

Implement sign-in sign-out buttons into pages/index.js

// pages/index.js
import { useSession, signIn, signOut } from "next-auth/react"
import useSWR from "swr";

const createFetcher = token => {
    const headers = new Headers();
    headers.append('Authorization', `Bearer ${token}`)
    return async (url) => fetch(url, { headers, mode: "cors" }).then(res => res.text())
}

const ApiData = () => {
    const { data: session } = useSession()
    const { data, error } = useSWR(`${session.apiEp}/api/HttpTrigger1?name=iwate`,createFetcher(session.idToken))
    if (error) return <div>failed to load</div>
    if (!data) return <div>loading...</div>
    return <div>{data}</div>
}

function HomePage() {
    const { data: session } = useSession()

    if (session) {
        return (
            <>
                Signed in as {session.user.email} <br />
                <ApiData/> <br/>
                <button onClick={() => signOut()}>Sign out</button>
            </>
        )
    }
    return (
        <>
            Not signed in <br />
            <button onClick={() => signIn('azure-ad-b2c')}>Sign in</button>
        </>
    )
}

export default HomePage

6. Set Environment Variable into SWA

Open Configuration panel of swa on azure portal. And set the fllowing env values:

  • AZURE_AD_B2C_TENANT_NAME: this is your if your aad b2c domain name is your.onmicrosoft.com
  • AZURE_AD_B2C_CLIENT_ID: Registered application ID
  • AZURE_AD_B2C_CLIENT_SECRET: Client secret for registered applciation
  • AZURE_AD_B2C_PRIMARY_USER_FLOW: A user flow name which is created on 2.
  • NEXTAUTH_URL: The URL of own swa.
  • NEXT_PUBLIC_API_EP: The URL of own backend functions.

7. Enable swa preview feature.

It need to modify github action workflow file for swa deply to execute SSR Next.js on swa.
Add the following environment variables to Azure/static-web-apps-deploy@v1 step

env: # Add environment variables here
  ENABLE_PREVIEW_FEATURES: true
  FUNCTION_LANGUAGE: node
  FUNCTION_LANGUAGE_VERSION: 16

8. Let's have fun!


You'll only receive email when they publish something new.

More from iwate
All posts