Blog

Email support on user first app login event

by | Oct 14, 2025 | Azure Technical | 0 comments

Introduction

In this blog, we demonstrate how to identify a user logging into an Azure-based application for the first time, then trigger an automated email alert to the Application Support Team.

The key to this solution is an Azure Alert that polls the Log Analytics workspace where application logins are captured.

SigninLog
| where AppId == ‘xxxx-xxxx-xxxx-xxxx-xxxx’
| where TimeGenerated > ago(5m)

If at least one sign-in has occurred in the last five minutes an Azure Runbook is called.
The runbook retrieves the application sign-in events from the Log Analytic workspace and summarises by user.
If a ‘first-time’ user is found an email notification is sent to the Application Support Team.

 

Example email notification:

From: Company <noreply@company.com.au>
Sent: Friday, 8 December 2023 9:55 AM
To: WW Business Projects <wwbusinessprojects@company.com.au>
Subject: New Process Manager application user – JONES Kelly

Dear Support,
A new user has signed into Process Manager for the first time, please complete onboarding activities.

Username: JONES Kelly
Email: KJONES@company.qld.gov.au
Directorate: Business Services
Branch: Chief Information Office
Department: Enterprise Platforms

The business unit will contact the user as required.
Kind regards,
City Process Manager Team

 

Alert Rule Configuration

Log Search Alert Rule


    "id": "/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx/resourceGroups/rg-p-ase-securitycontrols/providers/microsoft.insights/scheduledqueryrules/ProMapp",
    "name": "ProMapp",
    "type": "Microsoft.Insights/scheduledQueryRules",
    "location": "australiasoutheast",
    "tags": {
        "Business Owner": "Jim Smith",
        "Environment": "Production",
        "ICTSystem": "Azure Platform",
        "Security": "HighBIL"
    },
    "systemData": {
        "createdBy": "PSMITH@company.com.au",
        "createdByType": "User",
        "createdAt": "2023-11-21T21:48:20.4180184Z",
        "lastModifiedBy": "PSMITH@company.com.au",
        "lastModifiedByType": "User",
        "lastModifiedAt": "2023-11-26T22:06:17.7386834Z"
    },
    "properties": {
        "createdWithApiVersion": "2023-03-15-preview",
        "displayName": "ProMapp",
        "description": "",
        "severity": 3,
        "enabled": true,
        "evaluationFrequency": "PT5M",
        "scopes": [
            "/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx/resourceGroups/rg-p-ase-securitycontrols/providers/Microsoft.OperationalInsights/workspaces/la-p-ase-security"
        ],
        "targetResourceTypes": [
            "Microsoft.OperationalInsights/workspaces"
        ],
        "windowSize": "PT5M",
        "overrideQueryTimeRange": "P2D",
        "criteria": {
            "allOf": [
                {
                    "query": "SigninLogs\n| where AppId == 'xxxxxxxx-a25a-4615-ac10-xxxxxxxxxxx'\n| where TimeGenerated > ago(5m)\n",
                    "timeAggregation": "Count",
                    "dimensions": [],
                    "operator": "GreaterThanOrEqual",
                    "threshold": 1,
                    "failingPeriods": {
                        "numberOfEvaluationPeriods": 1,
                        "minFailingPeriodsToAlert": 1
                    }
                }
            ]
        },
        "autoMitigate": false,
        "actions": {
            "actionGroups": [
                "/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx/resourceGroups/rg-p-ase-securitycontrols/providers/microsoft.insights/actionGroups/ProMapp"
            ],
            "customProperties": {}
        }
    }
}

PowerShell Automation Runbook


#-----------------------------
# INITIALISE
#-----------------------------

Import-Module Az.OperationalInsights

#-----------------------------
# FUNCTIONS
#-----------------------------

function GetAccessTokenHeaders($tenantId, $appId, $appSecret){
    $uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
    $body = @{
        client_id     = $appId
        scope         = "https://graph.microsoft.com/.default"
        client_secret = $appSecret
        grant_type    = "client_credentials"
    }
    try{
        $tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
        # Unpack Access Token
        $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
        $headers = @{
            'Content-Type'  = "application\json"
            'Authorization' = "Bearer $Token"
        }
    } catch {
        $headers = $null
    }
    return $headers
}

# create the email message parameters - 26/04/22
function GetMessageParams($emailFrom, $emailTo, $htmlMsg, $headers, $userName){
    $recipientList = [System.Collections.ArrayList]@()
    $null = $recipientList.Add(@{emailAddress = @{address = ($emailTo)}})
    $bccRecipientList = [System.Collections.ArrayList]@()
    $null = $bccRecipientList.Add(@{emailAddress = @{address = ('psmith@company.com.au')}})
    return @{
        URI = "https://graph.microsoft.com/v1.0/users/$emailFrom/sendMail"
        Headers = $headers
        Method = "POST"
        ContentType = 'application/json'
        Body = (@{
            message = @{
                subject = "New Process Manager user - $userName"
                attachments = @(
                    @{
                        "@odata.type"= "#microsoft.graph.fileAttachment"
                        name = 'company_Logo'
                        contentType = 'image/gif'
                        contentBytes = $companyLogo
                    }
                )
                body = @{
                    contentType = 'HTML' 
                    content = $htmlMsg 
                }  
                toRecipients = $recipientList       
                bccRecipients = $bccRecipientList       
            }
        }
        ) | ConvertTo-JSON -Depth 6
    }   
}

# Build the email bogy
function BuildMessage($userDetails){
    return(
    
"<-html>
<-style>
{font-family: Arial; font-size: 13pt;}
TABLE{border: 1px solid black; border-collapse: collapse; font-size:13pt;}
TH{border: 1px solid black; background: #dddddd; padding: 5px; color: #000000;}
TD{border: 1px solid black; padding: 10px; Text-align:Center }
<-/style>
<-h3>Dear Support,<-/h3>
A new user has signed into Process Manager for the first time, please complete onboarding activities. 
<-br>
<-br>User name: <-b>$($userDetails.Name)<-/b>
<-br>Payroll: <-b>$($userDetails.Payroll)<-/b>
<-br>Email: $($userDetails.Email)
<-br>Directorate: <-b>$($userDetails.Directorate)<-/b>
<-br>Branch: <-b>$($userDetails.Branch)<-/b>
<-br>Department: <-b>$($userDetails.Department)<-/b>
<-br><-br>
The business unit will contact the user as required. 
<-br><-br>
Kind regards,
<-br>
City Process Manager Team
<-br>
<-br>
<-a href='https://www.company.com.au/default.html'>
<-img border=0 src='cid:CoGC_Logo'><-/a>
<-/body><-/html>"

    )
}

#-----------------------------
# MAIN
#-----------------------------

try{
    $errorMessage = 'unable to login using AA managed identity'
    Connect-AzAccount -Identity
    Set-AzContext 'Prod'
    $appId = $null # retrieved from Key Vault
    $appSecret = $null # retrieved from Key Vault
    $tenantId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx" # Azure tenant
    $recipientEmail = 'wwbusinessprojects@company.com.au'
    Write-Output "recipientEmail: $recipientEmail"
    # Get the log analytics workspace
    $errorMessage = 'OperationalInsightsQuery failed'
    $workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName "rg-p-ase-securitycontrols" -Name "la-p-ase-security"
    # get all past signins for ProMapp
    $query = "SigninLogs
        | where AppId == 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxx'"
    $result = Invoke-AzOperationalInsightsQuery -Workspace $workspace -Query $query
    # sumarise user sign-ins using a hash    
    $userSignIns = @{}
    $count = 0
    foreach($signIn in $result.Results){
        $userKey = "$($signIn.UserDisplayName.Split(' ')[-1])~$($signIn.UserPrincipalName)"
        if($userSignIns.Contains($userKey)){
            $data = $userSignIns.Get_Item($userKey)
            $data += $signIn.TimeGenerated
            $userSignIns.Set_Item($userKey,$data)
        } else {
            $userSignIns.Add($userKey,@($signIn.TimeGenerated))
        }
        $count ++
    }
    <# testing
        $userSignIns
        $userSignIns.Count
    #>
    Write-Output "$count application signins"
    Write-Output "$($userSignIns.Count) unique users"
    # check for sign-ins within last 5 minutes - only check users with one sign-in
    foreach($user in $userSignIns.GetEnumerator() | Where-Object{$_.Value.Count -eq 1}){
        $time = $user.value[0]
        $timeDiff = New-TimeSpan –Start $(Get-Date) –End $time
        $newUserCount = 0
        if($timeDiff.TotalMinutes -ge -5){
            Write-Output "Processing new user: $($user.Name), $time "
            $newUserCount ++
            if(-not $appId){
                # initialisation for first user
                $errorMessage = 'unable to access Key Vault'
                $appId = Get-AzKeyVaultSecret -VaultName kv-p-ae-cloudteam -Name AzureAutomationAppId -AsPlainText # only valid for PS 5.1
                $appSecret = Get-AzKeyVaultSecret -VaultName kv-p-ae-cloudteam -Name AzureAutomationAppSecret -AsPlainText # only valid for PS 5.1
# download the company logo file from Azure storage
                $errorMessage = 'unable to download logo file from storage'
                $sa = Get-AzStorageAccount -ResourceGroupName rg-p-ae-cloudteam -Name companyaecloudteam
                $null = Get-AzStorageBlobContent -Blob 'company_logo.png' -Container 'config' -Context $($sa.Context) -Destination "$env:TEMP\cogc_logo.png" -Force
                $cogcLogo = [convert]::ToBase64String((Get-Content "$env:TEMP\company_logo.png" -Encoding Byte -ReadCount 0))
                Write-Output 'Downloaded company logo file'
                $headers = GetAccessTokenHeaders $tenantId $appId $appSecret
                if(-not $headers){
                    $errorMessage = 'GetAccessTokenHeaders failed'
                    throw $null
                }
            }
            # login was within last 5 minutes
            # get user's details
            $userEmail = $user.Name.Split('~')[-1]
            $errorMessage = 'get user details failed'
            #$resourcesUrl = "https://graph.microsoft.com/v1.0/groups/xxxxxxx-250e-417f-8694-xxxxxxxxxxxx/members/microsoft.graph.user?`$select=employeeId,&`$expand=manager(`$select=displayName,userPrincipalName,employeeId,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcdirectoratedesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcbranchdesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcsectiondesc,jobTitle)"
            $resourcesUrl = "https://graph.microsoft.com/v1.0/users/$($userEmail)?`$select=displayName,userPrincipalName,employeeId,officeLocation,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcdirectoratedesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcbranchdesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogcsectiondesc,extension_eccb56601fe44c7584b1a2134dd30bc9_cogccostcentredesc,jobTitle&`$expand=manager"
            $response = Invoke-RestMethod -Method Get -Uri $resourcesUrl -Headers $headers -ContentType 'application/json'
            $userDetails = [PsCustomObject]@{
                Name = $response.displayName
                Payroll = $response.employeeId
                Email = $response.userPrincipalName
                Directorate = $response.extension_eccb56601fe44c7584b1a2134dd30bc9_cogcdirectoratedesc
                Branch = $response.extension_eccb56601fe44c7584b1a2134dd30bc9_cogcbranchdesc
                Department = $response.extension_eccb56601fe44c7584b1a2134dd30bc9_cogcsectiondesc
            }
            Write-Output ". Payroll: $($userDetails.Payroll)"
            Write-Output ". Email: $($userDetails.Email)"
            Write-Output ". Directorate: $($userDetails.Directorate)"
            Write-Output ". Branch: $($userDetails.Branch)"
            Write-Output ". Department: $($userDetails.Department)"
            # send email to user
            $htmlMsg = BuildMessage $userDetails 
            $MessageParams = GetMessageParams -emailFrom 'noreply@company.com.au' -emailTo $recipientEmail $htmlMsg $headers -userName $userDetails.Name
            $errorMessage = 'unable to send email'
            Invoke-RestMethod @Messageparams
            <# testing
                $MessageParams.Body.message | fl
            #>
            Write-Output 'Email notification sent'
        }
    }
} catch {
    Write-Output "ERROR: $errorMessage"
    Write-Output ". system message: $($_.Exception.Message)"
}

<# testing
    foreach($signIn in $result.Results){
        "$($signIn.UserDisplayName.Split(' ')[-1])~$($signIn.UserPrincipalName)"
    }
#>

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *