From PowerShell to TypeScript: Building a DMARC Report Management App with Vue, Quasar and Azure Static Web Apps

From PowerShell to TypeScript: Building a DMARC Report Management App with Vue, Quasar and Azure Static Web Apps
I have no design skills and couldn't pay for images on my free blog, so please excuse my use of absurd AI art.
💡
A quick note:
If you find a mistake - in my process, my understanding of the technologies, or my descriptions - please let me know. Part of the reason I'm writing this is to allow for the opportunity for other people to point out what I'm doing wrong so that I can learn from it. Thanks!

Intro

Recently, I've been stepping into some unfamiliar territory. I spend a lot of my working time writing automation, mostly using PowerShell. I work in Azure a lot, with Azure Functions and Logic Apps. Logic Apps allow me to work with on-prem databases, and I've found some pretty creative ways to move data between services in the past. It seems like every time I've thought that I would need to use a language like Python or JavaScript to accomplish a task, I've realized that PowerShell could easily handle it (and I already know PowerShell!).

But I keep having the itch to do more. I've wanted to contribute to open-source software for a while. I want to build my own apps. I want to create more robust interfaces that are more accessible for people with less technical skill. So, I went back to TypeScript. A couple years ago, I built a SPA using Vue's new-at-the-time Composition API with Quasar (and Webpack, not Vite). I figured it was worth revisiting Vue, since I was at least a little familiar with it. My plan was to write an app that could be used to manage DMARC reports, allowing easy searching and filtering, in addition to making it easier to spot misconfigurations and correlate malicious behavior over a large dataset. I've already been dumping the report data into a database for months, but we've been limited in our ability to make use of it.

So I chose my tools:

  • Vue 3 using the Composition API with TypeScript
  • Quasar 2 with Vite
  • MSAL.js 3 for Azure OAuth
  • GitHub with Actions for CI/CD
  • Azure Static Web Apps with Azure Functions for the API

I'm hosting the code in a private repo because it's for work. But I would like to document what I've done because there were several steps along the way that I had a lot of trouble with and I had a hard time finding tutorials for some of my use-cases. I'll try to provide links to helpful articles while I go through my process.


My First Hurdle

After scaffolding the project with Quasar CLI, the first thing I tried to do was to setup Azure OAuth. The tool that I'm building will be used in-house and anyone accessing the records should be authenticated with our Azure tenant and have the proper authorization. Microsoft provides the MSAL.js library, which makes it significantly easier to implement a secure authentication process, but comes with plenty of headaches along the way.

So, first of all, the best tutorial I found on the subject was by Kuntumallashivani on Medium. Her tutorial is easy to follow and pretty straightforward. However, make sure to visit her GitHub repo for this project because there are updates that address issues that I had encountered while working through the initial tutorial.

I also found this tutorial by Dave Stewart helpful, but I didn't want to go the route of using the popup for authentication. He points out issues with the redirect method, which does seem unnecessarily messy. I have had too much trouble authenticating with popups in the past, though.

This somewhat old (and strange) video from Microsoft DevRadio was also really helpful in understanding the process, though it's using MSAL.js 2, not 3.

Finally, even though it's buried in the repo, and as Dave Stewart rightly says in the above post, "The reality is that getting it working is a cavalcade of pain.", it's still worth looking at Microsoft's sample Vue app.

Notable Findings

One of the first things I found was that I needed to switch the vueRouterMode to history instead of hash. I know that there are ways to make it work properly without doing this, but:

  1. It was simple.
  2. I couldn't find anything that said it was bad practice.
  3. It only requires one small change to your staticwebapp.config.json to get it working properly when configuring your Azure Static Web App.

Honestly, most of the rest of my trouble with this step had to do with me not understanding components, state, and types. It doesn't feel useful to post my MSAL config functions because they are so similar to the configuration provided by Kuntumallashivani, but here are the two components that I'm using so far:

<template>
  <div class="auth">
    <div v-if="state.isAuthenticated">
      <q-btn 
        size="15px"
        color="red"
        @click="handleLogout"
      >
        Log Out
      </q-btn>
    </div>
    <div v-else>
      <q-btn 
        size="15px"
        color="green"
        @click="handleLogin"
      >
        Log In
      </q-btn>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';
import { useAuth } from '../config/useAuth';
import { myMSALObj, state } from '../config/msalConfig';

const { login, logout, handleRedirect } = useAuth();

const handleLogin = async () => {
  console.log('Logging in...');
  await login();
};

const handleLogout = async () => {
  console.log('Logging out...');
  logout();
};

const initialize = async () => {
  try {
    console.log('Initializing...');
    await myMSALObj.initialize();
  } catch (error) {
    console.log('Initialization error',error);
  }
};

onMounted(async () => {
  await initialize();
  await handleRedirect();
});
</script>


<style lang="scss">
.auth {
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>

AuthComponent.vue

<template>
    <div v-if="state.isAuthenticated" class="q-pa-md">
        <q-table
            v-if="data"
            class="my-sticky-header-table"
            flat bordered
            title="DMARC Records"
            :rows="data"
            :columns="columns"
            row-key="name"
            no-data-label="Goofed that one."
        />
    </div>
    <div v-else>Please log in to view table.</div>
  </template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { state } from '../config/msalConfig';

interface Column {
  name: string;
  label: string;
  field: string | ((row: string) => string);
  required?: boolean;
  align?: 'left' | 'center' | 'right';
  sortable?: boolean;
  sort?: ((a: string, b: string, rowA: string, rowB: string) => number) | undefined;
}

const columns: Column[] = [
  {
    name: 'guid',
    required: true,
    label: 'GUID',
    align: 'left',
    field: 'guid'
  },
  { name: 'dmarcDomain', align: 'center', label: 'DMARC Domain', field: 'dmarcDomain', sortable: true },
  { name: 'sourceIp', label: 'Source IP', field: 'sourceIp', sortable: true },
  { name: 'sourceIpCount', label: 'Source IP Count', field: 'sourceIpCount' },
  { name: 'dmarcDisposition', label: 'DMARC Disposition', field: 'dmarcDisposition' },
  { name: 'dmarcSpf', label: 'DMARC SPF', field: 'dmarcSpf' },
  { name: 'dmarcDkim', label: 'DMARC DKIM', field: 'dmarcDkim', sortable: true },
  { name: 'headerFrom', label: 'Header From', field: 'headerFrom', sortable: true }
]

const data = ref(null);

watch(
  () => state.isAuthenticated,
  () => {
    if (state.isAuthenticated) {
      loadData();
    }
  }
)

const loadData = async (): Promise<void> => {
    if (state.isAuthenticated) {
        console.log('Successfully authenticated.')
        try {
          data.value = await (await fetch('/api/retrieveRecords', {
            method: 'POST',
            body: JSON.stringify({ take: 10, skip: 0 }),
          })).json()
        } catch (error) {
          console.error('Error fetching data', error);
        }
    } 
}

onMounted(async () => {
  await loadData();
});
</script>

RecordViewer.vue


Environment Variable Hell

Once I got the basic login process and a sample API call to populate some data, I was ready to figure out how to get this into an Azure Static Web App. I'm used to pushing my Azure Functions written in PowerShell to GitHub and using GitHub Actions to deploy my code to my function apps. However, when working with Azure Functions, environment variables that you supply to your app in Azure are available at runtime. This isn't the case with a static site, since all the code on the front end is built during the initial deployment.

Ok, so I figured I would just put the environment variables in GitHub Actions secrets and inject them during the build process. Seemed simple enough. I quickly got completely confused.

First, I tried to use an env map to inject the secrets into the Quasar build step of the GitHub Actions workflow:

      - name: Build Quasar App
        env:
          VITE_DMARC_CLIENT_ID: ${{ secrets.VITE_DMARC_CLIENT_ID }}
          VITE_DMARC_AUTHORITY: ${{ secrets.VITE_DMARC_AUTHORITY }}
          VITE_DMARC_REDIRECTURI: ${{ secrets.VITE_DMARC_REDIRECTURI }}
        run: quasar build

I prefixed my variables with "VITE" after reading that Vite exposes these variables to the client.

This didn't work. I thought that maybe Quasar was receiving the variables, but didn't know what to do with them during the build process. So, I added an env map to the build object in quasar.config.js:

build: {
      env: {
        VITE_DMARC_AUTHORITY: process.env.VITE_DMARC_AUTHORITY,
        VITE_DMARC_CLIENT_ID: process.env.VITE_DMARC_CLIENT_ID,
        VITE_DMARC_REDIRECTURI: process.env.VITE_DMARC_REDIRECTURI,
      },
}

Nope.

One other thing I tried (that didn't work) was to use something like:

run: VITE_DMARC_REDIRECTURI=${{ secrets.VITE_DMARC_REDIRECTURI }} quasar build

I kept logging the values in my code, trying to prefix them with both process.env and import.meta.env (Vite's special environment object that replaces the values statically at build time). The values kept coming up as undefined.

This was almost definitely a rookie mistake, but I had a really hard time figuring out what I was doing wrong. I looked in the documentation for all of the tools I was using, I checked GitHub discussions and issues, I watched YouTube videos and Pluralsight courses. Eventually, I got help I needed from the Quasar Discord channel.

I ended up needing to do two things:

  1. Supply the env map in the root of the GitHub Actions workflow
    1. I don't completely understand why this is necessary. GitHub's documentation says that you can prefix environment variables with specific job steps, but it also seemed like you could supply an env map directly under a job. Either way, this is the only way it worked for me.
  2. Echo the values during the Quasar build line.
    1. Making the environment variables available to the overall deployment process doesn't seem to make them directly available to Quasar. Once I added the echoes to the "quasar build" line, everything started working.
name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main  # Change this to your default branch
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main  # Change this to your default branch

env:
  VITE_DMARCVUER_CLIENT_ID: ${{ secrets.VITE_DMARCVUER_CLIENT_ID }}
  VITE_DMARCVUER_AUTHORITY: ${{ secrets.VITE_DMARCVUER_AUTHORITY }}
  VITE_DMARCVUER_REDIRECTURI: ${{ secrets.VITE_DMARCVUER_REDIRECTURI }}
  VITE_BACKEND_API_URL: ${{ secrets.VITE_BACKEND_API_URL }}

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - name: Checkout GitHub Action
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Install dependencies
        run: npm install

      - name: Build API
        run: cd api && npm install && echo "$VITE_BACKEND_API_URL" && npm run build

      - name: Install Quasar CLI
        run: npm install -g @quasar/cli

      - name: Build Quasar App
        run: echo "$VITE_DMARCVUER_CLIENT_ID" && echo "$VITE_DMARCVUER_AUTHORITY" && echo "$VITE_DMARCVUER_REDIRECTURI" && quasar build

      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: 'upload'
          app_location: '/'
          api_location: 'api'
          output_location: 'dist/spa'

GitHub Actions workflow

With it setup like this, I was able to remove the env map from the build object in quasar.config.js and access the variables in my code. I tested accessing them with both process.env and import.meta.env prefixes and both worked. I ended up sticking with process.env.


I'm hoping that this post and others will be living documents. I imagine that I will learn things soon that will necessitate updates. I would like to spend a whole other post talking about setting up the API with Azure Functions using the Static Web Apps CLI to setup a local dev environment. It feels like so many pieces have fallen into place over the past few days.

So, if you have any feedback, leave a comment. If anything is wrong or unclear, let me know. Thanks for reading!