The cheap way to make multi tenant SaaS on Azure #1 | Poor man's Azure
November 1, 2022•1,089 words
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 isyour
if your aad b2c domain name isyour.onmicrosoft.com
AZURE_AD_B2C_CLIENT_ID
: Registered application IDAZURE_AD_B2C_CLIENT_SECRET
: Client secret for registered applciationAZURE_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