Automation with GitHub Workflows

Microsoft Graph PowerShell is a robust solution for automating tasks, executing batch operations, maintaining and ensuring consistency across different stages such as test, preproduction, and production environments. GitHub workflow is a configurable automated process that will run one or more jobs including PowerShell. Workflows are defined by a YAML file checked in to your repository and will run when triggered by an event in your repository, or they can be triggered manually, or at a defined schedule. Their benefits in accelerating and stabilizing the deployment process to Microsoft Entra's external ID. It leads to a significant reduction in integration issues, faster release cycles, enhance change management, and consistency that are crucial for maintaining data integrity and smooth and seamless deployment during updates and modifications.

1. Prepare your tenant

To allow GitHub workflow accessing your Microsoft Entra ID tenant, you first need to:
  1. Register an application
  2. Consent to the required permission. Each operations may require different permission. For more information check the Graph API documentation. You can also find the permissions and their IDs in the Microsoft Graph permissions reference.
  3. Create application secret

1.1 Register GitHub application

The following PowerShell script registers an application, grants admin consent to the required permissions, creates application secret and renders the tenant ID.

function Add-GitHubWorkflowApp {

    # Create app registration
    $params =  @{
        displayName = "GitHub workflow"
        description = "This application use to authentication the GitHub workflow"
        signInAudience = "AzureADMyOrg"
        requiredResourceAccess =  @(
            @{
                resourceAppId = "00000003-0000-0000-c000-000000000000"
                resourceAccess =  @(
                    @{
                        id = "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9"
                        type = "Role"
                    }
                    @{
                        id = "246dd0d5-5bd0-4def-940b-0421030a5b68"
                        type = "Role"
                    }
                    @{
                        id = "01c0a623-fc9b-48e9-b794-0756f8e8f067"
                        type = "Role"
                    }
                )
            }
        )
    }
    $appRegistration = New-MgApplication -BodyParameter $params
    Write-Host "App registration created with app ID"  $appRegistration.AppId

    # Create corresponding service principal
    $params =  @{
        appId = $appRegistration.AppId
    }    
    $servicePrincipal = New-MgServicePrincipal -BodyParameter $params
    Write-Host "Service principal created with ID"  $servicePrincipal.Id

    # Get Microsoft Graph service principal
    $graphServicePrincipal = Get-MgServicePrincipal -ServicePrincipalId appId='00000003-0000-0000-c000-000000000000'

    # Grant admin consent
    $roles = @('1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9', '246dd0d5-5bd0-4def-940b-0421030a5b68', '01c0a623-fc9b-48e9-b794-0756f8e8f067')
    foreach ($role in $roles)
    {
        $params = @{
            "PrincipalId" =$servicePrincipal.Id
            "ResourceId" = $graphServicePrincipal.Id
            "AppRoleId" = $role
        }

        New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $graphServicePrincipal.Id -BodyParameter $params  | Out-Null 
    }

    # Create an application secret
    $params =  @{
        passwordCredential =  @{
            displayName = "GitHub workflow app client secret"
        }
    }
    Add-MgApplicationPassword -ApplicationId $appRegistration.Id -BodyParameter $params | Format-List
}

# Connect to Microsoft Entra tenant with all the required scopes
Connect-MgGraph -Scopes "Application.ReadWrite.All AppRoleAssignment.ReadWrite.All"

# Run the script
Add-GitHubWorkflowApp

# Get your tenant ID
Get-MgContext | Format-List -Property TenantID

        

1.2 Copy the application details

Copy the tenant ID, application ID (client ID), and the client secret. You will use this information in your GitHub repository.

2. Prepare your GitHub repo

2.1 Create secrets

GitHub secrets allow you to store sensitive information in your organization, repository, or repository environments.
  1. On GitHub.com, navigate to the main page of the repository.
  2. Under your repository name, click Settings. If you cannot see the "Settings" tab, select the dropdown menu, then click Settings.
  3. In the "Security" section of the sidebar, select Secrets and variables, then click Actions.
  4. Select the Secrets tab.
  5. Click New repository secret.
  6. In the Name field, enter TenantId.
  7. In the Secret field, enter your tenant ID.
  8. Select Add secret.
  9. Repeat the last 4 steps and add ClientId with the client ID your registered. And ClientSecret with the application secret your created earlier.

2.2 [Optionally] Create variables

GitHub variables provide a way to store and reuse non-sensitive configuration information. You can store any configuration data such as compiler flags, usernames, or server names as variables. Variables are interpolated on the runner machine that runs your workflow. The example presented later in this page, use a variable name WebAppId which is passed into the PowerShell script that creates or update a conditional access policy.
  1. On GitHub.com, navigate to the main page of the repository.
  2. Under your repository name, click Settings. If you cannot see the "Settings" tab, select the dropdown menu, then click Settings.
  3. In the "Security" section of the sidebar, select Secrets and variables, then click Actions.
  4. Select the Variables tab.
  5. Click New repository variable.
  6. In the Name field, , enter a name for your variable.
  7. In the Secret field, enter the value for your variable.
  8. Select Add variable.

2.3 Add your PowerShell script

Add your PowerShell script to the root directory of the repository. The following example shows an example ConditionalAccessPolicy.ps1 script that creates or update a conditional access policy.

function Add-ConditionalAccessPolicy {

    param (
        $PolicyName,
        $AppId
    )

    # Define the conditional access policy
    $params =  @{
        templateId =  $undefinedVariable
        displayName = $PolicyName
        state = "enabled"
        sessionControls =  $undefinedVariable
        conditions =  @{
            userRiskLevels =  @()
            signInRiskLevels =  @(
                "high"
                "medium"
            )
            clientAppTypes =  @(
                "all"
            )
            platforms =  $undefinedVariable
            locations =  $undefinedVariable
            times =  $undefinedVariable
            deviceStates =  $undefinedVariable
            devices =  $undefinedVariable
            clientApplications =  $undefinedVariable
            applications =  @{
                includeApplications =  @(
                    $AppId
                )
                excludeApplications =  @()
                includeUserActions =  @()
                includeAuthenticationContextClassReferences =  @()
                applicationFilter =  $undefinedVariable
            }
            users =  @{
                includeUsers =  @(
                    "All"
                )
                excludeUsers =  @()
                includeGroups =  @()
                excludeGroups =  @()
                includeRoles =  @()
                excludeRoles =  @()
                includeGuestsOrExternalUsers =  $undefinedVariable
                excludeGuestsOrExternalUsers =  $undefinedVariable
            }
        }
        grantControls =  @{
            operator = "OR"
            builtInControls =  @(
                "mfa"
            )
            customAuthenticationFactors =  @()
            termsOfUse =  @()
            authenticationStrength =  $undefinedVariable
        }
    }

    # Try to find the policy by name
    $ca = Get-MgBetaIdentityConditionalAccessPolicy -Filter "displayName eq '$PolicyName'"

    # Create or update the conditional access policy
    if ($null -ne $ca ) {

        # Check the existence of multiple policies with the same name.
        if ($ca.Count -gt 1 ) {
            $policyCount = $ca.Count
            Write-Error -Message  "The operation could not be completed because $policyCount '$PolicyName' policies found in the directory."
            return    
        }

        Write-Host "Updating policy " $ca.Id
        Update-MgBetaIdentityConditionalAccessPolicy -ConditionalAccessPolicyId  $ca.Id -BodyParameter $params
        Write-Host "The conditional access policy has been successfully update"
    } else {
        Write-Host "Creating new policy"
        New-MgBetaIdentityConditionalAccessPolicy -BodyParameter $params | Format-List
        Write-Host "The conditional access policy has been successfully created"
    }
}

# Connect to Microsoft Entra tenant with the required scope
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"

# Run the script
Add-ConditionalAccessPolicy -PolicyName "Woodgrove demo - sign in risk" -AppId {App-ID}
        

1.3 Add a GitHub workflow

GitHub workflow is a configurable automated process that will run one or more jobs. Workflows are defined by a YAML file checked in to your repository and will run when triggered by an event in your repository, or they can be triggered manually, or at a defined schedule. Each workflow is stored as a separate YAML file in your code repository, in a directory named github/workflows.
  1. In your repository, create the .github/workflows/ directory to store your workflow files.
  2. In the .github/workflows/ directory, create a new file called ConfigWorkflow.yml.
  3. Add the following code
     
    # This is a basic workflow to help you get started with Actions
    
    name: Test Graph PowerShell
    
    # Controls when the workflow will run
    on:
    # Triggers the workflow on push request events but only for the "main" branch
    push:
        branches: [ "main" ]  
    
    # Allows you to run this workflow manually from the Actions tab
    workflow_dispatch:
    
    # A workflow run is made up of one or more jobs that can run sequentially or in parallel
    jobs:
    # This workflow contains a single job called "build"
    build:
        # The type of runner that the job will run on
        runs-on: ubuntu-latest
    
        # Steps represent a sequence of tasks that will be executed as part of the job
        steps:
        # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
        - uses: actions/checkout@v4
    
        # Run a command to check PowerShell version
        - name: Check PowerShell version
            shell: pwsh
            run: Write-Host $PSVersionTable.PSVersion
    
        # Setup cache for Microsoft Graph PowerShell
        - name: Setup PowerShell module cache
            id: cacher
            uses: actions/cache@v3
            with:
            path: "~/.local/share/powershell/Modules"
            key: ${{ runner.os }}-MicrosoftGraphBeta
    
        # Install Microsoft Graph PowerShell modules
        - name: Install required PowerShell modules
            if: steps.cacher.outputs.cache-hit != 'true'
            shell: pwsh
            run: |
            Set-PSRepository PSGallery -InstallationPolicy Trusted
            Install-Module Microsoft.Graph.Beta -ErrorAction Stop
    
        # Connect to Entra ID, run the PowerShell script and disconnect
        - name: Connect, run the script and disconnect
            shell: pwsh
            env:
            TenantId: ${{ secrets.TenantId }}
            ClientId: ${{ secrets.ClientId }}
            ClientSecret: ${{ secrets.ClientSecret }}
            WebAppId: ${{ vars.WebAppId }}
            run: |
            Write-Host "Connect to Microsoft Entra ID with app ID and app secret"
            $SecuredPassword = ConvertTo-SecureString -String "$env:ClientSecret" -AsPlainText -Force
            $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "$env:ClientId", $SecuredPassword
    
            Connect-MgGraph -TenantId "$env:TenantId" -ClientSecretCredential $ClientSecretCredential -NoWelcome
    
            Write-Host "Loading the PowerShell script .\ConditionalAccessPolicy.ps1"
            . .\ConditionalAccessPolicy.ps1
    
            Write-Host "Running the PowerShell script"
            Add-ConditionalAccessPolicy -PolicyName "Woodgrove demo - sign in risk" -AppId ${{ env.WebAppId }}
    
            Write-Host "Disconnect from Microsoft Entra ID"
            Disconnect-MgGraph  | Out-Null