Securing an Azure Static Web App with Auth0 Actions
A detailed walkthrough of securing a Blazor WASM based Azure Static Web App via RBAC powered by Auth0 Actions.
For our 1st code commit, we will initialize a default Blazor WASM application on our local workstation, deploy it on to Azure Static Web Apps, and then secure it with RBAC to verify the IAM is working as intended on the frontend. As the reading time foretells, this is an in-depth and step-by-step code walkthrough of deploying a production-ready app. Buckle up!!!
Most of the work was done over the release candidate versions, RC1 & RC2, but I have revalidated the codebase against the LTS version of .net 6 released yesterday [11th Nov'21].
This blog post is also available as Twitter Thread:
Local Development
In the past, for services similar to Azure SWA, developers had to follow a circuitous path of building the code on their local machine, followed by deploying it onto the cloud, and only then would they have been able to verify if their code performed as intended β especially true when the feature was infrastructure-dependent, like Identity and Access Management (IAM).
Azure SWA CLI
Azure SWA addresses the above pain point by providing a command-line tool that approximates the cloud environment: SWA CLI, and we will be relying on its core features to emulate authentication, custom routing, and route authorization.
Install the SWA CLI globally
npm install -g @azure/static-web-apps-cli
Since our Blazor WASM application will primarily address the organizer use cases, we will name it accordingly.
dotnet new blazorwasm -o OrganizerWeb
Ensure you are in the main git branch.
Immediately, you are confronted by the bane of modern software development: the spawning of way too many files.
Create a .gitignore to ensure the unnecessary files are not inadvertently checked-in.
dotnet new gitignore
Please don't forget to check-in the .gitignore file, thoughπ€·ββοΈ.
While creating the Blazor WASM project, .net will generate settings of application URLs with random ports. It's a matter of preference, but I like to standardize the port numbers [which will help us later when using the SWA CLI].
Let's kick the tires to ensure things are in working order.
dotnet watch
We also get to see in action one of the most touted features of .net 6: hot reload, which also generated a lot of heatπ₯ recently
Looks good.
Next up, we want the Azure SWA CLI to serve the Blazor WASM application. I prefer to execute the SWA commands at the solution level because it provides a vantage position for when we want to repeat this later when including the Azure Functions project(s).
swa start http://localhost:5000 --run "cd OrganizerWeb && dotnet watch run"
VoilΓ , the Blazor WASM served from the SWA CLI!!!
A page automatically opens in your browser, but that is powered by the .net watch command[please close this tab/browser; lest it will confuse you during development]. Note the URL with the port number 4280 and traverse to that location; this is the Azure SWA CLI rendered page.
SSL
Production web apps are served over HTTPS. Moreover, the default security posture adopted by browsers is to only open SSL-secured sites. Case in point, when I ran the watch run command above, .net opened the browser in https[port 7001] without a hitch. But traversing to the SWA CLI rendered page, which is being served over the ol' plain HTTP, the browser promptly disallowed it.
Using SSL right from the get-go ensures we catch any potential bugs related to it early. Unfortunately, SWA CLI repository's README skimps on the details.
One CLI SSL option errored out.
Providing only the SSL option to the CLI ain't cutting it.
What worked? A certificate and key generated by a Root Certificate Authority, trusted by the browser. I recommend mkcert for its ease of accomplishing this. Please ensure that the generated certificate, and key, are kept in a secure location. These should NOT be checked-in if generated within the repository folder; if you do so, amend the .gitignore file to exclude the key and certificate files.
swa start --ssl --ssl-cert="../localhost.pem" --ssl-key="../localhost-key.pem" https://localhost:7001 --run "cd OrganizerWeb && dotnet watch run"
To confirm in the terminal logs, note the HTTPS protocol of the SWA CLI rendered URL.
Traversing to the URL π
Azure SWA VS Code Extension
Now it is time to lob our app into the cloud. And we can do it right from the comforts of VS code itself, courtesy of the Azure SWA extension.
Ensure you have checked in all your code and have pushed them up to GitHub.
Assuming you have validated both your Azure & GitHub profile by the Azure SWA extension:
When clicking on the "plus" icon [circled in red below] of the extension, you will be prompted to select the Azure subscription, followed by a dialog box for entering the name of the Azure Static Web App project.
You will be asked for the Azure Region you wish to deploy the app. Right now, East Asia will be the nearest for the alpha users of the app.
Your frontend framework: choosing Blazor for this project.
Next, you will be asked for the source folder for your frontend project.
Finally, the build location for your frontend project.
If everything goes to plan, a VS Code toast notification about the successful completion of the GitHub actions will pop up. And within the Azure SWA extension, and tucked under the subscription it was created in, the Azure SWA project should be listed.
By right-clicking on the Azure SWA project, you will be presented with a context menu with one of the menu item called "Browse site". Clicking on it, you can traverse to your Azure SWA hosted page.
πππ Congratulations! You have successfully deployed your Blazor WASM app to Azure Static Web Apps. πππ
Things that tripped me
For folks from a TDD/BDD background who would usually begin by creating an Azure SWA project first and then proceed to code, unfortunately, the extension does not work on an empty folder.
Having said that, try to deploy your code asap. This project is not just our first project in Azure but also a re-entry into .net [coming from a 10-year Node.js background]. Tinkering with the generated gitignore, I un-commented the below line [brain freeze on my part that I want to attribute to working with ASP pre-2012π]. This led to an insidious error: the app worked fine on the local machine via SWA CLI, but on pushing the commits onto the cloud, GitHub actions failed because it could not find those files.
When you are creating a new Azure Static Web Apps project, the Azure SWA-generated GitHub action expects the artifacts to be on the default branch[main/master]. I had issues getting to work if I started with other git branches. However, once the Azure SWA is created, the GitHub action works very well with other branches in powerful ways, which we will see in "action" during our next section.
Authentication with pre-configured providers
Moving forward, like any good self-respecting production web app out there, our app will have certain pages that needs to be accessed only by authenticated users with authorized roles.
But before plunging headlong into the custom authentication, we will first ensure the IAM plumbing works with one of the Azure SWA pre-configured providers, GitHub.
Let's create a new git branch
git switch -c CustomAuth
Microsoft Learn has a great video series on Azure SWA, and one of them, Frank Boucher's authentication video, was crucial for this post.
Microsoft has released a .net library that configures an Authentication State Provider for our Azure SWA app.[ Don't go by its nomenclature. Microsoft is underselling it; it's not just for Azure Functionsπ]
dotnet add package Microsoft.Azure.Functions.Authentication.WebAssembly --prerelease
While developing this app in RC1 and RC2, there was only a prerelease version for use. I expected that we would have an official one when .net 6 was released. Alas, it is still in prerelease.
Also, this package's nuget page is bereft of any content for a potential user, and I have serious doubts about the GitHub repo it's claiming to be its source.
Once you add the library, we need to register the service in the WASM app builder found in the Program CSharp file: the 2 new lines added π [The structure of this page will be different if you are on .net 5.]
Next up, we need to bolt on the Authorization scaffolding to the Blazor App [This step is applicable for any Blazor WASM standalone app; it's not specific to Azure SWA]
Productivity tip: Use VS Code Format Document to get your code in this file to be indented automatically.
Let's update the layout of the Weather App by replacing the "About" link with a "Login/Logout" one.
Check out the URL highlighted within red: "Azure Static Web Apps uses the /.auth system folder to provide access to authorization-related APIs."
Run the SWA CLI command to bring up the Azure SWA app locally. When you click the Login link, you will come across one of the best features of SWA CLI: its emulation of the Azure SWA authentication infrastructure. The provider is pre-filled with the one we have in our path in the .auth link above, with a text box for the user name and a text area for Roles[for now, we leave it untouched]. Clicking on the Login button will lead you back to the index page with the Login link replaced with a message welcoming the username entered in the emulator page and a logout link.
Presently, anyone can access the Weather Forecast page [reachable at the /fetchdata route].
Let's change that. Adding just two lines in your FetchDate.razor file and a restart should make that happen.
Now, when an unauthenticated user clicks on the Fetch Data link in the navbar, the page will display a terse message instead of the weather forecast. "Note to self: It would be cool if the famous MC Hammer U can't touch this gif plays when an un-authenticated user clicks that link."
Hey, wait a minute, even John Smith from earlier can access the Weather Forecast page?!! Quite right, and here's the thing with "attribute [Authorize]" as it is, it only restricts anonymous access; once you are authenticated(doesn't matter as who), you are through.
So let's ensure only weather reporters can access the forecast page; we are now introducing Role-based Access Control[RBAC]. It requires a mere amendment to the Authorize Attribute.
And now, our authenticated but not having the requisite role aka unauthorized user cannot access the forecast page.
#TIL: David Letterman started his career as a weather reporter in the 70's.
So let's log out and then click on the Login link again to bring us back to the emulator. Only this time, we masquerade as David Letterman in the role of a weather reporter.
And lo and behold, David Letterman can access the weather forecast.
The combination of SWA CLI and Azure SWA VS Code extensions is a great way to get started with Azure SWA on your local machine. But it does not mean you can develop the app without an internet connection. The VS Code extension has to connect to the Azure infrastructure to do its job. But also the SWA CLI authentication emulator when using it for the first time since it needs to download a few static assets for the page.
Pull Request in Pre-Production
Great, now that we have corroborated on the workstation that the web app has a functioning IAM; let's validate it after deploying it on Azure.
Push the branch to GitHub.
git push -u origin CustomAuth
The GitHub dashboard for the project displays a helpful message about a git branch being available and persuading us to undertake a Pull Request.
And like any good OSS project, we follow this hygiene for merging a code into an upstream or main branch.
Remember the GitHub Actions generated for a newly created Azure SWA? It also gets triggered for any Pull Request on the main branch.
And here's a crucial productivity feature of Azure SWA: at the end of the successful processing of the GitHub action, the code will be available at a dedicated Stage URL. The Stage URL is almost identical to the production[main/master] branch URL, except for the subdomain appended with the GitHub Pull Request Number and Azure region.
You can test your stage URL independently or make a side-by-side comparison with the production branch.
Click on the login link[only available in codebase version hosted on the stage URL], and immediately you realize the training wheels are off when the browser redirects to the GitHub Sign-in page.
If you logged in for the first time, you need to consent for your personal information[email &/or username, depending on the provider] being shared with the app.
We have been authenticated, but we do not yet have access to the Weather Forecast page.
Why? David was doing a standup job as a weather reporter, BUT only on my local workstation [SWA CLI]. Over on the stage URL, it's my GitHub profile that is getting authenticated, which has not been assigned any role on the Azure Portal. Anyway, David has a flourishing career as a late-night TV host, and it is time we bid him adieu and have me, or to be precise, my GitHub profile, take over his weather reporting duties on the deployed app. Head on over to the Azure portal and visit the Role management page.
Can you grasp the significance of the last statement? We created an Azure SWA app, developed it locally, deployed it on Azure, created another branch to incorporate the IAM feature, hosted that version on its own stage URL β we did ALL that and did not have to visit the Azure Portal even once, till now. All accomplished from the comforts of the tools you are most familiar with: VS Code and GitHub. A remarkable win for developer productivity.
Azure SWA provides a built-in invitation system to assign roles to users when using pre-approved providers.
Oops, it looks like we need to revisit our code. Roles in Azure cannot contain hyphens. Replace the old role with weathercaster.
Test it locally, as we had done earlier.
Looks good.
Add the weathercaster role to my GitHub Profile name in the Azure Portal. And click on the invitation link generated below, which will redirect you to the main branch version of the web app[You can close this browser tab].
Push the changes to GitHub. Doing so will once again trigger the GitHub Actions, and at the end of its successful processing, it will generate a new comment with the same Stage URL.
Yay, my Github profile finally has access to the Weather Forecast page.
Configuration
You can control aspects of the Azure SWA via a JSON configuration file named staticwebapp.config.json. I like to create this file at the root of the Blazor WASM project.
Custom Routing
It's a good practice the login link has no reference to the authentication provider involved. And since we do not intend to use the other pre-configured providers[Twitter & Azure Active Directory], it's prudent from a security standpoint to have them disabled.
{
"routes": [
{
"route": "/login",
"rewrite": "/.auth/login/github"
},
{
"route": "/.auth/login/twitter",
"statusCode": 404
},
{
"route": "/.auth/login/aad",
"statusCode": 404
},
{
"route": "/logout",
"redirect": "/.auth/logout?post_logout_redirect_uri=/"
}
]
}
Startup the SWA CLI and replace the .auth links in MainLayout accordingly. You can see the hot reload in action too.
Verify the change on your browser's dev tools.
Clicking on the Login link should bring you to the SWA CLI emulator. Rinse & repeat.
Secure Routes by Roles
To be consistent with our code, we have to ensure that a direct request to the server for /fetchdata route of the Weather Forecast page is accessed only by authenticated users with the weathercaster role.
{
"route": "/fetchdata",
"allowedRoles": ["weathercaster"]
}
But, there's a caveat. Suppose we allow only folks with the role of "LordCount" to access the /counter route.
{
"route": "/counter",
"allowedRoles": ["LordCount"]
}
If there is an anonymous direct request to this route, then the platform will not serve the request.
But this does not prevent folks from accessing the route if the first request is to the index file [which is open to anonymous access]. It, in turn, ensures the whole WASM app gets loaded onto the user browser, and now anyone, including the unauthenticated, can access the counter page. [WhyY? The nature of the beast!!! Once a Single Page Application[SPA] is loaded onto the browser, the routes are traversed locally without invoking the Azure infrastructure]. And, unlike the fetchdata page, we have not secured the counter page via the Blazor Authorization implementation.
Fallback Routes
And because it's a SPA, we also need to serve those requests asked of the server for a route that does not adhere to any of the routing rules. [just like a default statement in a switch clause π]
"navigationFallback": {
"rewrite": "/index.html"
}
We will be revisiting this file as we implement more features in the coming weeks. It is worth your while to familiarize yourself thoroughly with different aspects of the config file. During my research, I found that one of the reasons that the CSP for the global header was somewhat lax was because of the browser refresh feature, which, coincidentally I raised just before the meltdown caused by the Hot Reload announcement.
Custom Authentication
Now we need to replace GitHub with our custom authentication provider, Auth0. A question might be swirling up in your mind: if the plan was to have Auth0 as our authentication provider, why was I messing around with GitHub? Here's the rub, custom authentication is not part of the Free plan for Azure Static Web Apps but that of the paid Standard Plan. Our strategy was to ensure we got the nuts and bolts of our frontend's IAM right, without incurring any costs.
Getting custom authentication to work involves a delicate dance amongst the three amigos: Azure SWA[portal + configuration file], Blazor WASM, and Auth0
Auth0
Aaron Powell's post has been a big help in configuring the Auth0 part.
Create an application, and note the following properties which we will need later on: a. Client ID b. Client Secret c. Domain Authority URL
As OIDC authentication rides piggyback on OAuth2 authorization flow, we have to configure the endpoints that Auth0 needs to redirect post the login process.
Azure Portal
Visit the Hosting Plan tab under the Static Web App in the Azure Portal to convert the plan from free to standard.
Click on the Configuration link within the navigation pane, choose the stage environment and add two applications secrets, namely AUTH0_CLIENT_ID, & AUTH0_CLIENT_SECRET, with the values from step 1 of the Auth0 subsection above.
Azure SWA Configuration
Back to our code, we provision custom authentication in our static web app config file.
{
"auth": {
"identityProviders": {
"customOpenIdConnectProviders": {
"auth0": {
"registration": {
"clientIdSettingName": "AUTH0_CLIENT_ID",
"clientCredential": {
"clientSecretSettingName": "AUTH0_CLIENT_SECRET"
},
"openIdConnectConfiguration": {
"wellKnownOpenIdConfiguration": "https://developerday-ind.us.auth0.com/.well-known/openid-configuration"
}
},
"login": {
"nameClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"scopes": [],
"loginParameterNames": []
}
}
}
}
}
}
Three things of note:
Named our custom provider as auth0.
The application secrets we provisioned in the settings on the portal above.
The well-known OpenID configuration endpoint, available at the Domain Authority URL.
Unfortunately, the Domain Authority URL was not considered sensitive enough to be secured as an application secret. And since we have to check in the code into a public GitHub repository, in the interim, I'm constrained to utilize an Auth0 developer tenant. [Tooting a bit of my own horn: This tenant was employed in the Auth0 hackathon in which I was runner-up: Used Auth0 Actions with Yoonik Face Recognition; I'll be expanding on this in the upcoming posts]
Also, we need to update the login route in the static file. And because we have implemented this route rule earlier, we don't need to modify the link within the MainLayout page.
{
"route": "/login",
"rewrite": "/.auth/login/auth0"
}
As we did with the other pre-configured providers, it's now time to thank GitHub for its service and disable it.
{
"route": "/.auth/login/github",
"statusCode": 404
}
Restart the CLI, and once you click on the Login link, the SWA CLI authentication emulator page comes up. We now see the provider field pre-populated with auth0 [the name we gave in the static config file], and I'm using my username as it is in the Auth0 tenant.
By now, you know the drill. Send up the git commits to GitHub and wait for the GitHub actions to complete. Once done, my Auth0 profile can haz some of that sweet Weather Forecast.
Custom Authorization
Update [13th Dec'21]: This section should have been the logical culmination of this blog post. Alas, I am encountering HTTP 500 errors on Azure SWA when incorporating this feature.
There are no logs available to dig through to find the cause, which was compounded by the total lack of response from the Azure team to our queries for days.
Since they are stakeholders waiting on this and the upcoming blog posts, I could not interminably wait on the Azure SWA team. To get the caravan going, I decided to:
- Create another git branch that will retrace the app a step back on production to the pre-approved providers feature
- Revert to the Free Plan. After all, we bumped up to the paid Standard Plan to have Custom Authentication, and since that is not working, it makes no sense to keep the meter running on the paid plan.
- Update this subsection after a solution from the Azure SWA team.
Auth0 Role Management
On the Auth0 end, we create a Role named "weathercaster"
Add my username to the role.
Auth0 Actions
We need to add the roles to the id token, and just for good measure, I'm also going to add them to the access token. In the past, for my older Auth0 tenants, this was achieved by Auth0 Rules and later by Auth0 Hooks. Last year, Auth0 introduced Actions to supersede them, and it's time we take the new hotness for a ride.
We are going to need a Custom Action
Create a Custom Action
The Monaco code editor opens up. And if you get a sense of dΓ©jΓ vu, that's because its also the one for VS Code. Adding a role to an OAuth2.0 token is such a common use case that you can find a code snippet for it, amongst others, after clicking the View Samples sticky button at the bottom right of the editor, which like the genius developer I am, have already copy-pasted into the editor.
We need to add the roles during our Login Flow.
Drag and drop our Custom Action into the Login Flow.
Apply the changes, and we are done on the Auth0 end.
"Trust, but verify": We used the Auth0 vanilla JS SPA sample to check that the Auth0 Action is indeed adding the roles to the ID Token.
Azure Function in Role Management
To glean out the custom roles from the response sent by an identity provider post-authentication, Azure SWA has a preview feature of using Azure Function to process the payload from the identity provider.
In the static config file, we need to enhance the auth section with the path of the Azure Function.
{
"auth": {
"rolesSource": "/api/GetRoles",
"identityProviders": {
// ...
}
}
}
But here is where things get awry. For our first iteration of the Azure Function, I wanted to play safe and used the sample tutorial. It's not even processing the payload, but rather hardcoding the result.
But pushing these changes to GitHub, and post a successful login [verified via Auth0 logs] on the Stage URL, Azure SWA throws up the dreaded internal server error.
Conclusion
Security is one of the pillars of a well-architected web application. And in my experience, it is always better to tackle it upfront because you have to depend on the ecosystem to get the job done. And if things go wrong, as it panned out here, you have slack in your project timeline to hustle up a Plan B.
Azure SWA is a productivity win for developers but so long as you use one of the pre-configured identity providers. For Custom Authentication, it's not yet ready for prime time.