Securing an Azure Static Web App with Auth0 Actions

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.

Β·

22 min read

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.

dotnet new command creating a Blazor WASM project with the terminal logs showing the output

Immediately, you are confronted by the bane of modern software development: the spawning of way too many files.

dotnet new creating 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πŸ€·β€β™‚οΈ. Cheeky Git commit message

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].

launch settings ports

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

Running .net watch command for hot reload

Looks good.

Running .net watch  the weather forecast page

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!!!

Azure SWA CLI terminal logs

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.

Default weather app served with Azure SWA CLI port number highlighted in the location bar

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.

Browser page displayed with the message of insecure connection

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. SWA CLI SSL option fail

  • Providing only the SSL option to the CLI ain't cutting it. SWA CLI option SSL fails with a message to provide key and cert

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.

SWA CLI terminal logs with hot reload, and SSL-enabled URL highlighted

Traversing to the URL πŸ‘‡

SWA rendered page with SSL lock icon clicked in the browser's location bar, revealing secure connection

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:

  1. 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. Azure SWA extension with the plus icon circled in red

  2. 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. Azure SWA extension dialogue box with a dropdown displaying Azure regions

  3. Your frontend framework: choosing Blazor for this project. Azure SWA extension dialogue box with a dropdown displaying different frontend frameworks

  4. Next, you will be asked for the source folder for your frontend project. Azure SWA extension dialogue box with a textbox filled with the source folder location

  5. Finally, the build location for your frontend project. Azure SWA extension dialogue box with a textbox filled with the build location

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.

Azure SWA extension with Azure SWA listed under the subscription it was created

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. πŸŽ‰πŸŽ‰πŸŽ‰ Azure SWA context menu

Things that tripped me

  1. 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. VS Code toast notification of error message of empty workspace

  2. 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πŸ˜‰]. wwwroot entry in gitignore 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.

  3. 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. Azure SWA GitHub action fail by running on default Git branch 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.

  1. 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. VS Code terminal showing dotnet add package failing with a request to use prerelease version

    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.

  2. 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.] Program.cs with horizontal green markers at the start of the new lines added to the file

  3. 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] Blazor WASM's app razor page with code added for authentication highlighted

    Productivity tip: Use VS Code Format Document to get your code in this file to be indented automatically. VS Code Command Palette dropdown with Format Document as the first option

  4. Let's update the layout of the Weather App by replacing the "About" link with a "Login/Logout" one. Pre and post of the main layout page

    Check out the URL highlighted within red: "Azure Static Web Apps uses the /.auth system folder to provide access to authorization-related APIs."

  5. 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. SWA CLI emulation of Azure SWA authentication 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. Azure SWA app's index page with the user name highlighted

  6. Presently, anyone can access the Weather Forecast page [reachable at the /fetchdata route].

    Weather Forecast page with the Login

    Let's change that. Adding just two lines in your FetchDate.razor file and a restart should make that happen.

    FetchData.razor file with the code addition highlighted in green

    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."

    Weather Forecast page displaying "Not Authorized" message which is highlighted in red

  7. 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.

    The weather forecast is displayed with John Smith as logged in user

    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. fetch data code with Authorize attribute amended with weather-reporter role

    And now, our authenticated but not having the requisite role aka unauthorized user cannot access the forecast page. Weather Forecast page displays a message of "Not authorized" with John Smith as the logged in user

    #TIL: David Letterman started his career as a weather reporter in the 70's. David Letterman in front of weather map

    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. Azure SWA CLI emulator with username as David Letterman and role as weather-reporter

    And lo and behold, David Letterman can access the weather forecast. Weather Forecast page successfully displayed with David Letterman highlighted in green

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. SWA CLI emulator page along with Developer Tools console showing

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.

GitHub project dashboard with pull request button

And like any good OSS project, we follow this hygiene for merging a code into an upstream or main branch. GitHub pull request page

Remember the GitHub Actions generated for a newly created Azure SWA? It also gets triggered for any Pull Request on the main branch. GitHub actions running on pull request

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. GitHub Pull Request Page showing the automated comment with the stage URL. The Pull Request on the page is highlighted, and so too is the Stage URL in the comments

You can test your stage URL independently or make a side-by-side comparison with the production branch. Two browser tabs resized to fit screen side by side for comparison of the production branch and the Stage 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. 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. Azure consent page when visiting the site for the first time

We have been authenticated, but we do not yet have access to the Weather Forecast page. Weather Forecast page on stage with username highlighted in green and the page message of "Unauthorized access" highlighted in red

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. Azure SWA Role invitation tab

Oops, it looks like we need to revisit our code. Roles in Azure cannot contain hyphens. Replace the old role with weathercaster. Role in Authorize attribute changed to weathercaster in Fetch data razor page

Test it locally, as we had done earlier. SWA CLI authentication emulator page with the username filled with Github Profile name and weathercaster added to the list of roles

Looks good. Weather Forecast page with username as GitHub profile name

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]. Role Management page in Azure Portal with weathercaster entered in the role text area

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. Visage GitHub Actions page with Azure SWA comments with stage URL

Yay, my Github profile finally has access to the Weather Forecast page. Weather Forecast page on the Stage URL with GitHub user profile

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.

VS Code Explorer showing location of staticwebapp json within the solution hierarchy

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. Main layout with custom routes highlighted in red

Verify the change on your browser's dev tools. SWA CLI rendered page with Edge Dev Tools opened and the Login link element's href attribute highlighted

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. SWA CLI emulator rendered a 401 page

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. Counter page accessed with a Login link showing that page is open to anonymous access

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. Dev Tools console showing the CSP error flagged by the browser

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.

  1. Create an application, and note the following properties which we will need later on: a. Client ID b. Client Secret c. Domain Authority URL Auth0 Application Properties

  2. As OIDC authentication rides piggyback on OAuth2 authorization flow, we have to configure the endpoints that Auth0 needs to redirect post the login process. Auth0 OAuth2 Endpoints

Azure Portal

  1. Visit the Hosting Plan tab under the Static Web App in the Azure Portal to convert the plan from free to standard. Hosting Plan of the Azure Static Web App showing the features of both the Free and Standard Plan

  2. 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 page with the secrets added

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:

  1. Named our custom provider as auth0.

  2. The application secrets we provisioned in the settings on the portal above.

  3. 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. SWA CLI render emulator page

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. Weather forecast with the Auth0 username highlighted

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. Azure SWA displaying a message about internal server error

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

  1. On the Auth0 end, we create a Role named "weathercaster" Auth0 Add Role section with the name and description fields filled out

  2. Add my username to the role. Auth0 add the user 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.

  1. We are going to need a Custom Action Auth0 Action Library

  2. Create a Custom Action Create Auth0 Action Modal Page

  3. 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. Auth0 Action Code Editor with code snippet

  4. We need to add the roles during our Login Flow. Auth0 Action Flow Page

  5. Drag and drop our Custom Action into the Login Flow. Auth0 Action

  6. Apply the changes, and we are done on the Auth0 end. Auth0 Action Applied

  7. "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. Browser page showing the profile sent by Auth0, with the dev tools console also showing the same

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.

VS Code Editor with left pane showing the API code, right pane the result of the invocation of the function, and the bottom terminal with the command to start the function

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.