functions/New-ServicePrincipalObject.ps1

Function New-ServicePrincipalObject {
    <#
    .SYNOPSIS
        PowerShell module for creating, retrieving and removing Azure registered / enterprise applications and service principals.
 
    .DESCRIPTION
        This module will create creating, retrieving and remove Azure registered / enterprise applications and service principals objects
        that can be used for automation tasks. Enterprise applications created will also have a mirror service principal objects created. Azure
        registered applications will be linked to a new service principal which can then be use as an authentication mechanism for connecting applications
        and PowerShell session to an Office 365 and Azure tenant.
 
        The PSServicePrincipal logging provider is based on PSFramework: All messages are logged by default 'Documents\PowerShell Script Logs' on
        Windows and 'Documents/PowerShell Script Logs' on MacOS. There are two logging streams (output and debug). Both streams have respective
        folders for analysis: You can run Get-LogFolder -LogFolder [OutputLoggingFolder] and [DebugLoggingFolder] to access either logging stream directory.
 
        For more information on PSFramework please visit: https://psframework.org/
        PSFramework Logging: https://psframework.org/documentation/quickstart/psframework/logging.html
        PSFramework Configuration: https://psframework.org/documentation/quickstart/psframework/configuration.html
        PSGallery - PSFramework module - https://www.powershellgallery.com/packages/PSFramework/1.0.19
 
    .PARAMETER CreateSelfSignedCertificate
        Used when creating a single self-signed certificate to be used with registered and enterprise applications for certificate based connections.
 
    .PARAMETER CreateSingleObject
        Used when creating a single default enterprise application (service principal).
 
    .PARAMETER CreateBatchObjects
        Used when creating a batch of service principals from a text file.
 
    .PARAMETER CreateSPNWithAppID
        Used when creating a service principal and a registered Azure ApplicationID.
 
    .PARAMETER CreateSPNWithPassword
        Used when creating a service principal and a registered Azure application with a user supplied password.
 
    .PARAMETER CreateSPNsWithNameAndCert
        Used when creating a service principal and a registered Azure application using a display name and certificate.
 
    .PARAMETER Cba
        Used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments.
 
    .PARAMETER EnableException
        Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts.
 
    .PARAMETER RegisteredApp
        Switch used to create an Azure registered application.
 
    .PARAMETER Reconnect
        Used when forcing a new connection to an Azure tenant subscription.
 
    .PARAMETER ApplicationID
        Unique ApplicationId for a service principal in a tenant. Once created this property cannot be changed.
 
    .PARAMETER Certificate
        This parameter is the value of the "asymmetric" credential type. It represents the base 64 encoded certificate.
 
    .PARAMETER DisplayName
        DisplayName of the objects you are retrieving.
 
    .PARAMETER NameFile
        Name of the file that contains the list of service principals being passed in for creation.
 
    .PARAMETER ObjectID
        ObjectId for a service principal in a tenant. Once created this property cannot be changed.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -Reconnect
 
        Force a reconnect to a specific Azure tenant.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -CreateSelfSignedCertificate
 
        Create a basic self-signed certificate to be used with registered and enterprise applications for certificate-based connections.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSingleObject
 
        Create a new service principal with a display name of 'CompanySPN' and password (an autogenerated GUID) and creates the service principal based on the application just created. The start date and end date are added to password credential.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSingleObject -CreateSPNWithPassword
 
        Create a new Enterprise Application and service principal with a display name of 'CompanySPN' and a (user supplied password) and creates the service principal based on the application just created. The start date and end date are added to password credential.
 
     .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanyApp -CreateSingleObject -RegisteredApp -Cba
 
        Create a registered application with a display name of 'CompanyApp', a self-signed certificate which is uploaded to the application and applies the correct appRoll assignments.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e -CreateSingleObject -CreateSPNWithAppID
 
        Create a new Enterprise Application and service principal with the ApplicationID '34a23ad2-dac4-4a41-bc3b-d12ddf90230e'.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -CreateBatchObjects -NameFile c:\temp\YourDataFile.txt
 
        Connect to an Azure tenant and creates a batch of enterprise applications and service principal objects from a file passed in.
 
    .EXAMPLE
        PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSPNsWithNameAndCert -CreateSingleObject -Certificate <public certificate as base64-encoded string>
 
        Create a new Enterprise Application and service principal with a display name of 'CompanySPN' and certifcate and creates the service principal based on the application just created. The end date is added to key credential.
 
    .NOTES
        When passing in the application ID it is the Azure ApplicationID from your registered application.
 
        WARNING: If you do not connect to an Azure tenant when you run Import-Module Az.Resources you will be logged in interactively to your default Azure subscription.
        After signing in, you will see information indicating which of your Azure subscriptions is active. If you have multiple Azure subscriptions in your account and
        want to select a different one, get your available subscriptions with Get-AzSubscription and use the Set-AzContext cmdlet with your subscription id.
 
        INFORMATION: The default parameter set uses default values for parameters if the user does not provide any.
        For more information on the default values used, please see the description for the given parameters below.
        This cmdlet can assign a role to the service principal with the Role 'Contributor'. Roles are applid at the end of the service principal creation.
        For more information on Azure RBAC roles please see: https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
 
        Microsoft TechNet Documentation: https://docs.microsoft.com/en-us/powershell/module/az.resources/new-azadserviceprincipal?view=azps-3.8.0
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType('System.Boolean')]
    [OutputType('System.String')]

    param(
        [parameter(ParameterSetName = "Reconnect")]
        [switch]
        $Reconnect,

        [parameter(Mandatory = $True, ParameterSetName = "DisplayName", HelpMessage = "Display name used to create or delete an SPN or application")]
        [ValidateNotNullOrEmpty()]
        [string]
        $DisplayName,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch indicating to create a single service principal or application")]
        [switch]
        $CreateSingleObject,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for indicating to create self-signed certificate and upload it to the Azure application")]
        [switch]
        $Cba,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for indicating we want to create a Registered Azure application")]
        [switch]
        $RegisteredApp,

        [parameter(ParameterSetName = "CreateSelfSignedCertificate")]
        [switch]
        $CreateSelfSignedCertificate,

        [parameter(Mandatory = $True, ParameterSetName = 'CreateBatchObjects', HelpMessage = "Switch used to create batch objects")]
        [switch]
        $CreateBatchObjects,

        [parameter(Mandatory = $True, ParameterSetName = 'CreateBatchObjects', HelpMessage = "Name file used to create a batch of spn's")]
        [ValidateNotNullOrEmpty()]
        [string]
        $NameFile,

        [parameter(ParameterSetName = "ID", HelpMessage = "ApplicationID used to create or delete an SPN or application")]
        [ValidateNotNullOrEmpty()]
        [Guid]
        $ApplicationID,

        [parameter(ParameterSetName = "ID", HelpMessage = "Switch for creating spn with an ApplicationID")]
        [switch]
        $CreateSPNWithAppID,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for creating spn with a certificate and Base64 string")]
        [switch]
        $CreateSPNsWithNameAndCert,

        [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for creating spn with a password")]
        [switch]
        $CreateSPNWithPassword,

        [parameter(HelpMessage = "Certificate parameter for a created spn")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Certificate,

        [parameter(HelpMessage = "Enable exception logging")]
        [switch]
        $EnableException
    )

    process {

        # Adding admin check for Windows Version 2004 which needs admin access due to UAC
        if(-NOT (Test-PSFPowerShell -Elevated))
        {
            Write-PSFMessage -Level Host -Message "PSServicePrincipal security components need PowerShell to run as an administrator. Exiting" -StringValues $module
            return
        }

        $script:certCounter = 0
        $script:certExportedCounter = 0
        $script:runningOnCore = $false
        $script:AzSessionFound = $false
        $script:AdSessionFound = $false
        $script:AzSessionInfo = $null
        $script:AdSessionInfo = $null
        $script:roleListToProcess = New-Object -Type System.Collections.ArrayList
        $requiredModules = @("AzureAD", "Az.Accounts", "Az.Resources")

        Foreach ($module in $requiredModules) {
            if (-NOT (Get-Module -Name $module)) {
                Import-Module $module; Write-PSFMessage -Level Verbose -Message "Importing required modules {0}" -StringValues $module
            }
        }

        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Reconnect
        Write-PSFMessage -Level Host -Message "Starting script run: {0}" -StringValues (Get-Date)

        if ($DumpCerts) {
            Write-PSFMessage -Level Host -Message "Opening CurenntUser and LocalMachine Certificate Stores"
            Get-ChildItem Cert:\CurrentUser\My | Sort-Object
            Get-ChildItem Cert:\LocalMachine\My | Sort-Object
            return
        }

        if ($CreateSelfSignedCertificate) {
            Invoke-PSFProtectedCommand -Action "Attempting to create self-signed certificate!" -Target $parameter.Values -ScriptBlock {
                New-SelfSignedCert @parameter -EnableException -ErrorAction Stop
            } -EnableException $EnableException -PSCmdlet $PSCmdlet
        }

        Invoke-PSFProtectedCommand -Action "Attempting to create self-signed certificate!" -Target $parameter.Values -ScriptBlock {
            Connect-ToCloudTenant @parameters -EnableException
            return
        } -EnableException $EnableException -PSCmdlet $PSCmdlet

        if ($CreateBatchObjects) {
            Write-PSFMessage -Level Host -Message "Testing access to {0}" -StringValues $NameFile

            if (-NOT (Test-Path -Path $NameFile)) {
                Stop-PSFFunction -Message "ERROR: File problem. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                return
            }
            else {
                Write-PSFMessage -Level Host -Message "{0} accessable. Reading file contents" -StringValues $NameFile
                $objectsToCreate = Get-Content $NameFile | Where-Object { $_ -ne "" }

                # Validate that we have data and if we dont we exit out
                if (0 -eq $objectsToCreate.Length) {
                    Stop-PSFFunction -Message "Error with imported content. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                    return
                }
            }

            # Check to make sure we have the list of objects to process
            if (-NOT $objectsToCreate) {
                Write-PSFMessage -Level Warning "ERROR: No list of objects found!"
                return
            }

            Write-PSFMessage -Level Host -Message "Object list DETECTED! Staring batch creation of SPN's"

            if ($RegisteredApp) {
                Write-PSFMessage -Level Host -Message "Creating batch registered Applications"
                if (-NOT $script:runningOnCore) {
                    foreach ($DisplayName in $objectsToCreate) {
                        try {
                            $newApp = New-AzureADApplication -DisplayName $DisplayName -ErrorAction SilentlyContinue -ErrorVariable ProcessError
                        }
                        catch {
                            Stop-PSFFunction -Message "ERROR creating batch spn's" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                            return
                        }

                        if ($newApp) {
                            Write-PSFMessage -Level Host -Message "Registered Application created: DisplayName: {0} - ApplicationID {1}" -StringValues $newApp.DisplayName, $newApp.AppId
                            $script:roleListToProcess.Add($newApp)

                            # Since we only create an AzureADapplicaiaton we need to create the matching service principal
                            try {
                                New-ServicePrincipal -ApplicationID $newApp.AppID
                            }
                            catch {
                                Stop-PSFFunction -Message "ERROR creating batch spn's" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                                return
                            }
                        }
                        elseif ($ProcessError) { Write-PSFMessage -Level Warning "WARNING: {0}" -StringValues $ProcessError.Exception.Message }
                    }

                    if ($script:roleListToProcess.Count -gt 0) { Add-RoleToSPN -spnToProcess $script:roleListToProcess }
                }
                else {
                    Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6 to create Registered Applications."
                }
            }
            else {
                Write-PSFMessage -Level Host -Message "Creating batch entperise service principal and applications"
                foreach ($spn in $objectsToCreate) { New-ServicePrincipal -DisplayName $spn }

                if ($roleListToProcess.Count -gt 0) { Add-RoleToSPN -spnToProcess $script:roleListToProcess }
            }
        }

        if ($CreateSingleObject) {
            try {
                if ($RegisteredApp) {
                    Write-PSFMessage -Level Host -Message "Creating registered applications"
                    if (-NOT $script:runningOnCore) {
                        if ($Cba) { New-SelfSignedCert -CertificateName $DisplayName -SubjectAlternativeName $DisplayName -Cba -RegisteredApp -EnableException }
                        else { $newApp = New-AzureADApplication -DisplayName $DisplayName -ErrorAction SilentlyContinue -ErrorVariable ProcessError }

                        if ($newApp) {
                            Write-PSFMessage -Level Host -Message "Registered Application created: DisplayName: {0} - ApplicationID {1}" -StringValues $newApp.DisplayName, $newApp.AppId
                            New-ServicePrincipal -ApplicationID $newApp.AppId -RegisteredApp # Since we only create an AzureADapplicaiaton we need to create the matching service principal
                        }
                        elseif ($ProcessError) { Write-PSFMessage -Level Warning "WARNING: $($ProcessError[0].Exception.Message)" }
                    }
                    else {
                        Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6 to create Registered Applications."
                    }
                }
                elseif ($DisplayName -and $CreateSPNWithPassword) { New-ServicePrincipal -DisplayName $DisplayName -CreateSPNWithPassword }
                elseif (($DisplayName) -and (-NOT $RegisteredApp) -and (-NOT $Cba) -and (-NOT $CreateSPNsWithNameAndCert)) { New-ServicePrincipal -DisplayName $DisplayName }

                if ($script:roleListToProcess.Count -gt 0) {
                    Add-RoleToSPN -spnToProcess $script:roleListToProcess
                    if ($Cba) { Add-ExchangePermsToSPN.ps1 -DisplayName $DisplayName }
                }
            }
            catch { Stop-PSFFunction -Message "ERROR: Creating a simple SPN failed" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ }
        }

        if ($CreateSPNWithAppID) {
            try {
                New-ServicePrincipal -ApplicationID $ApplicationID
            }
            catch {
                Stop-PSFFunction -Message "ERROR creating an spn by application id" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                return
            }
        }

        if ($CreateSPNsWithNameAndCert) {
            try {
                if ((-NOT $Certificate)) {
                    Stop-PSFFunction -Message "ERROR: No service principal specified. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet
                    return
                }
                else {
                    Write-PSFMessage -Level Host -Message "Creating new SPN {0} and with auto generated certificate key" -StringValues $DisplayName
                    $endDate = Get-Date; $endDate = $currentDate.AddYears(1)
                    $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -CertValue $Certificate -EndDate $endDate -ErrorAction Stop
                    Add-RoleToSPN -spnToProcess $newSPN
                }
            }
            catch {
                Stop-PSFFunction -Message "ERROR: No certificate as base64-encoded string specified. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_
                return
            }
        }
    }

    end {
        Write-PSFMessage -Level Host -Message "End script run: {0}" -StringValues (Get-Date)
        Write-PSFMessage -Level Host -Message 'Log saved to: "{0}". Run Get-LogFolder to retrieve the output or debug logs.' -StringValues $script:loggingFolder
    }
}