Private/_Auth.ps1

function New-PACAuth {
    param(
        [Parameter(Mandatory = $false)] [string] $ServerUrl = $global:devops_ServerUrl
    )

    # Delete existing pac auth for PPDO
    & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth delete --name ppdo

    if ($global:devops_DataverseCredType -in ("servicePrincipal", "user")) {
        # Create new pac auth for PPDO, depending on Credential Type
        if ($global:devops_DataverseCredType -eq "servicePrincipal") {
            & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth create --applicationId $global:devops_ClientID --clientSecret `"$global:clientSecret`" --environment $ServerUrl --tenant $global:devops_TenantID  --name ppdo
        }
        else {
            & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth create --username $global:devops_DataverseEmail --password `"$global:Password`" --environment $ServerUrl --name ppdo
        }

        # Select new PPDO connection
        & $env:APPDATA\Capgemini.PowerPlatform.DevOps\PACTools\tools\pac.exe auth select --name ppdo
    }
}

function Get-AccessToken {
    Write-Verbose "Getting Updated Access Token"
    $azureDevopsResourceId = "499b84ac-1321-427f-aa17-267ca6975798"
    $token = az account get-access-token --resource $azureDevopsResourceId | ConvertFrom-Json
    $env:AZURE_DEVOPS_EXT_PAT = $token.accessToken
}

#region Azure
function Connect-PPDOAzure {
    param(
        [string] $TenantId = $null,
        [string] $SubscriptionId = $null
    )

    function Test-AzureSubscriptionAccess {
        param(
            [Parameter(Mandatory)] [System.Object] $Account,
            [switch] $ErrorOnInvalidAccount
        )
        
        az account set --subscription $Account.SubscriptionId
        $global:devops_selectedSubscription = $Account.SubscriptionId
        $global:devops_selectedSubscriptionName = $Account.SubscriptionName

        $loggedInUsername = (az account show | ConvertFrom-Json).user.name
        if ($null -eq $loggedInUsername -or $loggedInUsername -ne $Account.Username) {
            Write-PPDOMessage -Message "Unable to find credentials for the subscription '$global:devops_selectedSubscriptionName'. You may encounter further errors if you continue." -Type "error" -RunLocally $true
            pause
            return $false
        }

        $credentialTestResult = az account list-locations
        if ($null -ne $credentialTestResult) {
            Write-Verbose "Credentials are valid"
            return $true
        }
        elseif ($ErrorOnInvalidAccount) {
            Write-PPDOMessage -Message "Failed to connect to Azure Subscription '$global:devops_selectedSubscriptionName'. You may encounter further errors if you continue." -Type "error" -RunLocally $true
            pause
            return $false
        }
        # Log out of account with expired credentials
        $usernameMessage = ""
        $expiredUsername = (az account show | ConvertFrom-Json).user.name
        if ($null -ne $expiredUsername) {
            $usernameMessage = "for account $expiredUsername "
            az logout --username $expiredUsername
        }

        Write-PPDOMessage -Message "Interactive login required $usernameMessage(likely due to MFA expiring)" -Type "warning" -RunLocally $true
        return $false
    }

    Write-Verbose "Attempting to connect to Azure using tenant id: '$TenantId' and subscription id: '$SubscriptionId'"
    $selectedAccount = $null

    if (![string]::IsNullOrEmpty($TenantId)) {
        Write-Verbose "Tenant ID is not null"
        if (!(Test-IsGuid($TenantId))) {
            Write-PPDOMessage -Message "Invalid Tenant Id '$TenantId' is not a GUID." -Type "error" -RunLocally $true
            pause
            return
        }
        Write-Verbose "Tenant ID is a GUID"
    }
    
    if (![string]::IsNullOrEmpty($SubscriptionId)) {
        Write-Verbose "Subscription ID is not null"
        if (!(Test-IsGuid($SubscriptionId))) {
            Write-PPDOMessage -Message "Invalid Subscription Id '$SubscriptionId' is not a GUID." -Type "error" -RunLocally $true
            pause
            return
        }
        Write-Verbose "Subscription ID is a GUID"
    }
    
    # If the Azure Subscription was provided
    if (![string]::IsNullOrEmpty($SubscriptionId)) {
        $selectedAccount = Get-AzureAccountForSubscription($SubscriptionId)
    }
    else {
        $tenantFilter = $null
        if ($TenantId -ne "") {
            $tenantFilter = "?tenantId == '$TenantId'"
        }
        $AZAccounts = az account list --query "[$tenantFilter].{Username:user.name, SubscriptionId:id, SubscriptionName:name, TenantId:tenantId}" | ConvertFrom-Json

        # If at least one account was found for the Azure Tenant
        if ($AZAccounts.Count -gt 0) {
            [array]$AZoptions = "Login to a New Account"
            $AZoptions += $AZAccounts | ForEach-Object { "$($_.SubscriptionName) $($_.Username) ($($_.TenantId))" }

            $sel = $null
            do {
                $sel = Invoke-Menu -MenuTitle "------ Please Select your Subscription ------" -MenuOptions $AZoptions          
            } until ($sel -ge 0)
            if ($sel -gt 0) {
                $selectedAccount = $AZAccounts[$sel - 1]
            }
        }
    }

    # If existing credentials were found / selected
    if ($null -ne $selectedAccount) {
        # If credentials are valid, return.
        if (Test-AzureSubscriptionAccess -Account $selectedAccount) {
            return
        }
    }
    
    # Interactive Login
    if ([string]::IsNullOrEmpty($TenantId)) {
        $Result = az login --allow-no-subscriptions
    }
    else {
        $Result = az login --tenant $TenantId
    }
    
    if (!$Result) {
        Write-PPDOMessage -Message "Login failed. You may encounter further errors if you continue." -Type "error" -RunLocally $true
        pause
        return
    }

    $NewAccount = $null
    # If subscription wasn't provided, set the Account directly
    if ([string]::IsNullOrEmpty($SubscriptionId)) {
        $NewAccount = az account show --query "{Username:user.name, SubscriptionId:id, SubscriptionName:name, TenantId:tenantId}" | ConvertFrom-Json
    }
    else {
        # Get the account against the subscription
        $NewAccount = Get-AzureAccountForSubscription($SubscriptionId)
    }

    if ($null -eq $NewAccount) {
        Write-PPDOMessage -Message "The provided credentials do not have access to Azure Subscription $SubscriptionId. You may encounter further errors if you continue." -Type "error" -RunLocally $true
        pause
        return
    }

    # Test the new connection works / was setup correctly
    Test-AzureSubscriptionAccess -Account $NewAccount -ErrorOnInvalidAccount
}

function Get-AzureAccountForSubscription {
    param(
        [Parameter(Mandatory, Position = 0)] [Guid] $SubscriptionId
    )
    Write-Verbose "Looking for account with subscription id: $SubscriptionId"
    $Account = az account list --query "[?id == '$SubscriptionId'].{Username:user.name, SubscriptionId:id, SubscriptionName:name, TenantId:tenantId}" | ConvertFrom-Json
    if ($null -ne $Account -and $null -ne $Account[0].SubscriptionId) {
        Write-Verbose "Found an existing account for subscription $SubscriptionId"
        Write-Verbose "$($Account[0])"
        return $Account
    }
    return $null
}

#endregion
#region Dataverse

function Get-AzureAccountsForDataverse($reason) {
    [array]$options = "Login to a new Account"
    $AZCredentials = az account list --query '[].{Username:user.name, Type:user.type, SubscriptionId:id, SubscriptionName:name, TenantId:tenantId}' --output json | ConvertFrom-Json
    $UniqueCredentials = $AZCredentials | Sort-Object -Property * -Unique 
    $options += $UniqueCredentials | ForEach-Object { "$($_.Username) ($($_.Type))($($_.SubscriptionName))($($_.TenantId))" }

    do {
        $sel = Invoke-Menu -MenuTitle "---- Please Select your Subscription ($reason) ------" -MenuOptions $options   
    } until ($sel -ge 0)
    if ($sel -eq 0) {
        if ($global:devops_isDocker) {
            az login --use-device-code --allow-no-subscriptions    
        }
        else {
            az login --allow-no-subscriptions
        }
        Get-AzureAccountsForDataverse($reason)
    }
    else {
        $global:devops_DataverseEmail = $UniqueCredentials[$sel - 1].Username
        $global:devops_DataverseADSubscription = $UniqueCredentials[$sel - 1].SubscriptionId
        $global:devops_DataverseCredType = $UniqueCredentials[$sel - 1].Type
        $global:devops_HasDataverseLogin = $true
    }
}

function Get-DataverseLogin {
    Param(
        [bool] [Parameter(Mandatory = $false)] $overrideSP = $false,
        [bool] [Parameter(Mandatory = $false)] $requiresManagementApi = $true
    )
    if (!$global:devops_HasDataverseLogin -or ($global:devops_projectFile.OverrideSPCheck -eq "True")) {
        Write-Host "Connecting to Dataverse Environment"
        Write-Host ""

        [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12    
        if (($global:devops_ClientID -and $overrideSP -eq $false) -and !($global:devops_projectFile.OverrideSPCheck -eq "True" -and $requiresManagementApi -eq $true)) {
            $global:devops_DataverseCredType = "servicePrincipal"
        }
        else {
            Get-AzureAccountsForDataverse("Power Platform")
        }

        if ($global:devops_DataverseCredType -eq "user") {
            [String]$userString = $global:devops_DataverseEmail
            Connect-PowerPlatform($userString, "https://management.azure.com/")
            Connect-PowerPlatform($userString, "https://service.powerapps.com/")
        }
        else {
            Write-PPDOMessage -Message "Using Service Principal" -RunLocally $true
            if ($global:devops_projectFile.ClientSecretAKVName) {
                Connect-PPDOAzure -TenantId $global:devops_TenantID -SubscriptionId $global:devops_selectedSubscription
            
                Write-PPDOMessage -Message "Retrieving Secret from Key Vault" -RunLocally $true
                $KVRetrieve = az keyvault secret show --name $global:devops_projectFile.ClientSecretAKVName --vault-name $global:devops_projectFile.AzureKeyVaultName | ConvertFrom-Json
                $global:clientSecret = $KVRetrieve.value   
                Write-PPDOMessage -Message "Secret Retrieved" -RunLocally $true
            }
            else {       
                if (!$global:clientSecret) {
                    Write-Host "Retrieving Secret from Local Store"
                    $clientKeySS = ($global:devops_configFile.Projects[$global:devops_projectConfigID].ClientSecret) | ConvertTo-SecureString
                    $global:clientSecret = (New-Object PSCredential "user", $clientKeySS).GetNetworkCredential().Password
                    Write-Host "Secret Retrieved" 
                }     
                else { Write-Host "Secret Provided by Command Line/Cache" }
            }
        }

        try {
            Add-PowerAppsAccount -ApplicationId $global:devops_ClientID -ClientSecret "$global:clientSecret" -TenantID $global:devops_TenantID
            $ManagementApps = Get-PowerAppManagementApps 
            $SPAdminPortalAccess = $ManagementApps.value | Where-Object applicationId -eq $global:devops_ClientID
            $SPAdminPortalAccess = $SPAdminPortalAccess.applicationId
            Write-Host "Service Principal Check Override : $($global:devops_projectFile.OverrideSPCheck)"
            if (!$SPAdminPortalAccess -and ($global:devops_projectFile.OverrideSPCheck -eq "False")) {
                $EnableAccess = Read-Host -Prompt "Service Principal $global:devops_ClientID does not have permissions to administer Power Platform, would you like to [e]nable access, login as another [u]ser or [c]ontinue without Power Platform admin (some actions will not be available)? [e/u/c]"
                if ($EnableAccess.ToLower() -eq 'e') {
                    Write-Host "Please Login to Power Platform with a User who has access to Administer Environments"
                    $global:currentSession.loggedIn = $false
                    $tempClientID = $global:devops_ClientID
                    $global:devops_ClientID = $null
                    Get-DataverseLogin
                    $global:devops_ClientID = $tempClientID
                    $global:devops_DataverseCredType = "servicePrincipal"
                    $SPAdminPortalAccess = New-PowerAppManagementApp -ApplicationId $global:devops_ClientID
                    if ($SPAdminPortalAccess.StatusCode -eq "403") {
                        Throw $SPAdminPortalAccess
                    }
                    $global:currentSession.loggedIn = $false
                    Add-PowerAppsAccount -ApplicationId $global:devops_ClientID -ClientSecret "$global:clientSecret" -TenantID $global:devops_TenantID
                    $global:devops_HasDataverseLogin = $true
                    Write-Host "Connected to Dataverse"
                }
                elseif ($EnableAccess.ToLower() -eq 'u') {
                    Get-DataverseLogin -overrideSP $true
                }
                elseif ($EnableAccess.ToLower() -eq 'c') {
                    $global:devops_HasDataverseLogin = $true
                    Write-Host "Connected to Dataverse"
                }
                else {
                    Throw $SPAdminPortalAccess
                }
            }
            else {
                $global:devops_HasDataverseLogin = $true
                Write-Host "Connected to Dataverse"
            }
        }
        catch {
            $global:devops_HasDataverseLogin = $false
            Write-Host $_
            pause
        }
    }
}

function Connect-PowerPlatform() {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True)]$args       
    )
    $theUser = $args[0]
    if ($args[1]) {
        $theAudience = $args[1]        
    }
    else {
        $theAudience = "https://management.azure.com/"
    }
    $authBaseUri =
    switch ($Endpoint) {
        "usgovhigh" { "https://login.microsoftonline.us" }
        "dod" { "https://login.microsoftonline.us" }
        default { "https://login.windows.net" }
    };

    [string]$Endpoint = "prod"

    [string]$Audience = $theAudience
    
    [string]$CertificateThumbprint = $null
    [string]$ClientSecret = $null

    $getToken = az account get-access-token --resource $Audience -s $global:devops_DataVerseADSubscription
    if (!$getToken) {
        Write-Host "Re-Authenticating $theUser"
        if ($global:devops_isDocker) {
            az login --use-device-code --allow-no-subscriptions    
        }
        else {
            az login --allow-no-subscriptions
        }
        Connect-PowerPlatform($theUser, $Audience)
    }

    $authResult = $getToken | ConvertFrom-Json

    $claims = Get-JwtTokenClaimsForPA -JwtToken $authResult.AccessToken

    $global:currentSession = @{
        loggedIn              = $true;
        idToken               = $authResult.IdToken;
        upn                   = $claims.upn;
        tenantId              = $claims.tid;
        userId                = $claims.oid;
        applicationId         = $claims.appid;
        certificateThumbprint = $CertificateThumbprint;
        clientSecret          = $ClientSecret;
        secureClientSecret    = $SecureClientSecret;
        refreshToken          = $authResult.RefreshToken;
        expiresOn             = (Get-Date).AddHours(8);
        resourceTokens        = @{
            $Audience = @{
                accessToken = $authResult.AccessToken;
                expiresOn   = $authResult.ExpiresOn;
            }
        };
        selectedEnvironment   = "~default";
        authBaseUri           = $authBaseUri;
        flowEndpoint          = 
        switch ($Endpoint) {
            "prod" { "api.flow.microsoft.com" }
            "usgov" { "gov.api.flow.microsoft.us" }
            "usgovhigh" { "high.api.flow.microsoft.us" }
            "dod" { "api.flow.appsplatform.us" }
            "china" { "api.powerautomate.cn" }
            "preview" { "preview.api.flow.microsoft.com" }
            "tip1" { "tip1.api.flow.microsoft.com" }
            "tip2" { "tip2.api.flow.microsoft.com" }
            default { throw "Unsupported endpoint '$Endpoint'" }
        };
        powerAppsEndpoint     = 
        switch ($Endpoint) {
            "prod" { "api.powerapps.com" }
            "usgov" { "gov.api.powerapps.us" }
            "usgovhigh" { "high.api.powerapps.us" }
            "dod" { "api.apps.appsplatform.us" }
            "china" { "api.powerapps.cn" }
            "preview" { "preview.api.powerapps.com" }
            "tip1" { "tip1.api.powerapps.com" }
            "tip2" { "tip2.api.powerapps.com" }
            default { throw "Unsupported endpoint '$Endpoint'" }
        };            
        bapEndpoint           = 
        switch ($Endpoint) {
            "prod" { "api.bap.microsoft.com" }
            "usgov" { "gov.api.bap.microsoft.us" }
            "usgovhigh" { "high.api.bap.microsoft.us" }
            "dod" { "api.bap.appsplatform.us" }
            "china" { "api.bap.partner.microsoftonline.cn" }
            "preview" { "preview.api.bap.microsoft.com" }
            "tip1" { "tip1.api.bap.microsoft.com" }
            "tip2" { "tip2.api.bap.microsoft.com" }
            default { throw "Unsupported endpoint '$Endpoint'" }
        };      
        graphEndpoint         = 
        switch ($Endpoint) {
            "prod" { "graph.windows.net" }
            "usgov" { "graph.windows.net" }
            "usgovhigh" { "graph.windows.net" }
            "dod" { "graph.windows.net" }
            "china" { "graph.windows.net" }
            "preview" { "graph.windows.net" }
            "tip1" { "graph.windows.net" }
            "tip2" { "graph.windows.net" }
            default { throw "Unsupported endpoint '$Endpoint'" }
        };
        cdsOneEndpoint        = 
        switch ($Endpoint) {
            "prod" { "api.cds.microsoft.com" }
            "usgov" { "gov.api.cds.microsoft.us" }
            "usgovhigh" { "high.api.cds.microsoft.us" }
            "dod" { "dod.gov.api.cds.microsoft.us" }
            "preview" { "preview.api.cds.microsoft.com" }
            "tip1" { "tip1.api.cds.microsoft.com" }
            "tip2" { "tip2.api.cds.microsoft.com" }
            default { throw "Unsupported endpoint '$Endpoint'" }
        };
    };
}

function Get-JwtTokenClaimsForPA {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]$JwtToken
    )

    $tokenSplit = $JwtToken.Split(".")
    $claimsSegment = $tokenSplit[1].Replace(" ", "+").Replace("-", "+");
    
    $mod = $claimsSegment.Length % 4
    if ($mod -gt 0) {
        $paddingCount = 4 - $mod;
        for ($i = 0; $i -lt $paddingCount; $i++) {
            $claimsSegment += "="
        }
    }

    $decodedClaimsSegment = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($claimsSegment))

    return ConvertFrom-Json $decodedClaimsSegment
}

function Get-DataverseConnection {
    Param(
        [string] [Parameter(Mandatory = $true)] $DeployServerUrl
    )
    try {
        Get-DataverseLogin -requiresManagementApi $false
        if ($global:devops_DataverseCredType -eq "user") {
            try {
                if ($global:devops_isDocker) {
                    Write-Host "Warning: It is recommended you configure and use a Service Principal instead of Username and Password (to prevent MFA related issues)" -ForegroundColor Yellow
                    Write-Host ""
                    $SecurePassword = Read-Host "Enter Password for $global:devops_DataverseEmail" -AsSecureString
                    $global:Password = (New-Object PSCredential "user", $SecurePassword).GetNetworkCredential().Password
                    [string]$CrmConnectionString = "AuthType=OAuth;Username=$global:devops_DataverseEmail;Password=$global:Password;Url=$DeployServerUrl;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;TokenCacheStorePath=$env:APPDATA\Capgemini.PowerPlatform.DevOps\dataverse_cache.data;LoginPrompt=Never"
                }
                else {
                    [string]$CrmConnectionString = "AuthType=OAuth;Username=$global:devops_DataverseEmail;Password=$global:Password;Url=$DeployServerUrl;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;TokenCacheStorePath=$env:APPDATA\Capgemini.PowerPlatform.DevOps\dataverse_cache.data;LoginPrompt=Auto"
                }
                $conn = Connect-CrmOnline -ConnectionString $CrmConnectionString -ConnectionTimeoutInSeconds 600

                return $conn
            }
            catch {
                Write-Host $_
                pause
            }
        }
        elseif ($global:devops_DataverseCredType -eq "servicePrincipal") {
            try {
                [string]$CrmConnectionString = "AuthType=ClientSecret;Url=$DeployServerUrl;ClientId=$global:devops_ClientID;ClientSecret='$global:clientSecret'"
                $conn = Connect-CrmOnline -ConnectionString $CrmConnectionString -ConnectionTimeoutInSeconds 600
                Write-Verbose "Ran Connect-CrmOnline with Service Principal"
                if (!$conn.IsReady -and $conn.LastCrmError.Contains("Invalid Login")) {
                    $AddSPAsSysAdmin = Read-Host -Prompt "The Service Principal $global:devops_ClientID does not have access to $DeployServerUrl, would you like to add access now ? [y/n]"
                    if ($AddSPAsSysAdmin.ToLower() -eq "y") {
                        try {
                            $global:currentSession.loggedIn = $false
                            Add-D365ApplicationUser -d365ResourceName $DeployServerUrl -servicePrincipal $global:devops_ClientID -roleNames "System Administrator"                            
                        }
                        catch {
                            Write-Host $_
                            pause
                        }           
                        
                        #add CLI auth create
                        $conn = Connect-CrmOnline -ConnectionString $CrmConnectionString -ConnectionTimeoutInSeconds 600
                    }
                }
                return $conn                      
            }
            catch {
                Write-Host $_
                pause
            }
        }
        else {
            try {
                $conn = Connect-CrmOnline -ServerUrl $DeployServerUrl -ConnectionTimeoutInSeconds 600 -ForceOAuth   
                return $conn                    
            }
            catch {
                Write-Host $_
                pause
            }
        }
    }
    catch {
        Write-Host $_
        pause
    }
}