Install-SecureMFA_SSPR_Portal.ps1

#Requires -RunAsAdministrator
#Requires -Version 5.0

<#
     .SYNOPSIS
        Installs SecureMFA SSPR Portal on empty Windows Server.
    .DESCRIPTION
        Installs SecureMFA SSPR Portal on empty Windows Server and applies connection configuration to the backend database amd IIS application pool.
 
        Dependencies:
            * Server is Microsoft Windows x64 architecture and can run IIS 10 and above.
            * Deployment account must be member or local computer administrators� group to install successfully.
            * Windows Hosting Bundle Installer deployed for ASP.NET Core 3.1 Runtime (minimum v3.1.10).
              The installer URL can be obtained from:
              https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-aspnetcore-3.1.10-windows-hosting-bundle-installer
              Specify a path to downloaded file using -dotnet_hosting_bundle_path parameter to install it during SSPR Portal installation.
            * If you access SQL service with windows integrated authentication , you must have service account created and configured on IIS Application pool which runs your SSPR Portal.
  
    .NOTES
        Version: 2.0.0.1
        Author: SecureMfa.com
        Creation Date: 06/01/2021
        Purpose/Change: New Release
   
    .EXAMPLE
        C:\PS> Install-SecureMFA_SSPR_Portal -sqlserver "asqlaol1.adatum.labnet,1433" -sqldbname "SecureMfaOTP" -sqlintegratedsecurity $true
 
        This command will Installs IIS applies configuration to IIS Application Pools and configures SecureMFA SSPR Portal on empty Windows Server.
    
#>


#Static Parameters
$WebSSPRPortalSource = (Join-Path -Path $PSScriptRoot -ChildPath SecureMFASSPR.zip)
$serverFQDN = (([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname).tolower()
$websspr_service_account = "NA";
$ConnectionString = ""
$sqluseraccount = "sa"
$sqluserpassword = ""

#Check if windows events source for application log exist, if not create one.
if ([System.Diagnostics.EventLog]::SourceExists("SecureMFA SSPR") -eq $False) {New-EventLog -LogName "Application" -Source "SecureMFA SSPR" ; Write-Host "SecureMFA SSPR Log Source Created."}

Function Install-SecureMFA_SSPR_Portal {
Param
(
    [Parameter(Mandatory=$false)][string]$sqlserver = "asqlaol1.adatum.labnet",
    [Parameter(Mandatory=$false)][string]$sqldbname = "SecureMfaOTP",
    [Parameter(Mandatory=$false)][bool]$sqlintegratedsecurity = $true,
    [Parameter(Mandatory=$false)][string]$siteName = "SecureMFASSPR",
    [Parameter(Mandatory=$false)][string]$WebSSPRPortalPath = "C:\inetpub\SecureMFASSPR\",
    [Parameter(Mandatory=$false)][string]$IISAppPoolName = "SecureMFASSPR",
    [Parameter(Mandatory=$false)][string]$dotnet_hosting_bundle_path = "c:\temp\dotnet-hosting-3.1.10-win.exe",
    [Parameter(Mandatory=$false)][Switch]$Force
)
    
    #START
        
    if (!$Force) {
    $message  = "Please confirm if you want to install SecureMFA SSPR Portal [Old deployment for web site $siteName will be deleted and reinstalled into $WebSSPRPortalPath ]"            
    $question = 'Please confirm?'
    $choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
    $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
    $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
    $decision_Validation = $Host.UI.PromptForChoice($message, $question, $choices, 0)
    if ($decision_Validation -eq 1 ) {Write-Host "Deployment has been cancelled, exiting!" -ForegroundColor Yellow ; break} 
    }
    
    #Read sql usrname and password if windows integrated security is not used
    if(!$sqlintegratedsecurity) 
    {
    write-host "Please enter sql account details for web.config file configuration." -ForegroundColor Cyan
    $sqluseraccount = $( Read-Host "Enter sqluseraccount")
    $sqluserpasswordsec = $( Read-Host "Enter sqluserpassword" -AsSecureString )
    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sqluserpasswordsec)
    $sqluserpassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 
    }
            
    try
    {
        $Error.Clear()
        if (!(Test-Path $WebSSPRPortalSource -Type Leaf) ) { throw "$WebSSPRPortalSource does not exist" ; break}
            
        #Start deployment

        #Deploy IIS Service with SecureMFA Web API Portal
        #Install-WindowsFeature Web-Server,Web-Net-Ext45,Web-Asp-Net45,Web-Mgmt-Console,Web-Scripting-Tools,Web-Http-Logging
        Install-WindowsFeature Web-Server,Web-Filtering,Web-Mgmt-Console,Web-Scripting-Tools,Web-Stat-Compression,Web-Http-Logging,Web-Static-Content,Web-Http-Errors,Web-Dir-Browsing,Web-Default-Doc

        #Site Configuration
        Import-Module WebAdministration

        $dotnet_core_hosting_module = Get-WebGlobalModule | where-object { $_.name.ToLower() -like "aspnetcoremodule*" }
        if (!$dotnet_core_hosting_module)
        {
            if (!(Test-Path $dotnet_hosting_bundle_path -Type Leaf) ) { throw "Cannot deploy dotnet hosting bundle because file does not exist in $dotnet_hosting_bundle_path" ; break}
            Write-Host "Installing dotnet hosting bundle $dotnet_hosting_bundle_path ..."
            Start-Process $dotnet_hosting_bundle_path -ArgumentList "/install /passive /quiet /norestart" -Wait -NoNewWindow -PassThru
        }
        else
        {
            Write-Host "dotnet hosting bundle has been allready installed."
        }

        #Create a new IIS app pool
        if (Test-Path ("IIS:\AppPools\" + $IISAppPoolName)) { Write-Host "$IISAppPoolName IIS AppPool exists. Skipping configuration ..." -ForegroundColor Yellow  } else {

                Write-Host "Creating $IISAppPoolName IIS AppPool ..."
                $appPool = New-WebAppPool -Name $IISAppPoolName

                $message  = "Do you want to set up App pool account on IIS server for website $siteName (Recommended)"            
                $question = 'Please confirm?'
                $choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
                $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
                $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
                $decision_Validation2 = $Host.UI.PromptForChoice($message, $question, $choices, 0)
                  
                #Select YES
                if ($decision_Validation2 -eq 0) {

                    #Configure service account for IIS apppool
                    $websspr_service_account = Read-Host "Enter service account username for IIS application pool $IISAppPoolName in following format DOMAIN\USERNAME "
                    do {
                    if($inputfailures -eq 0) {Write-Host "Please enter a password for $IISAppPoolName AppPool Service Account $websspr_service_account :"} else {Write-Host "[Retry: $inputfailures] Passwords didn't match for $websspr_service_account Try again:"}
                    $pwd1 = Read-Host "Password" -AsSecureString
                    $pwd2 = Read-Host "Re-enter Password" -AsSecureString
                    $pwd1_text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd1))
                    $pwd2_text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd2))
                    $inputfailures++
                    }
                    while ($pwd1_text -ne $pwd2_text )
                    $fscredential = new-object -typename System.Management.Automation.PSCredential -argumentlist $websspr_service_account,$pwd1        
        
                    Set-ItemProperty "IIS:\AppPools\$IISAppPoolName" -name processModel.identityType -Value SpecificUser 
                    Set-ItemProperty "IIS:\AppPools\$IISAppPoolName" -name processModel.userName -Value $fscredential.UserName
                    Set-ItemProperty "IIS:\AppPools\$IISAppPoolName" -name processModel.password -Value $pwd1_text
                }
                else {Write-Host "App pool account creation has been cancelled, skipping ..." -ForegroundColor Yellow ;}                

        }

        #Create WEB API portal
         
        #Stop IIS website and app pool if it was deployed before.
        $waitunload = $false
        if ((Get-IISAppPool -Name $IISAppPoolName).state -eq "Started") {Stop-WebAppPool -Name $IISAppPoolName ; write-host "$IISAppPoolName app pool has been stoped."; $waitunload = $true} else {write-host "$IISAppPoolName app pool is not running."}
        if ((Get-Website -Name $siteName).state -eq "Started") {Stop-Website $siteName ; write-host "$siteName website has been stoped."; $waitunload = $true} else {write-host "$siteName website is not running."}
        
        if($waitunload) {write-host "Waiting for $siteName to unlaod ..." ; Start-Sleep -s 10}       

        if(!(Test-Path ($WebSSPRPortalPath))){New-Item -ItemType Directory -Force -Path $WebSSPRPortalPath} else {write-host "Folder already exist: $WebSSPRPortalPath , cleaning up!." -ForegroundColor Yellow ; Remove-Item $WebSSPRPortalPath -Recurse -Force ; New-Item -ItemType Directory -Force -Path $WebSSPRPortalPath}

        Expand-Archive -LiteralPath $WebSSPRPortalSource -DestinationPath $WebSSPRPortalPath -Force

        New-WebSite -Name $siteName -Port 80 -HostHeader $serverFQDN -PhysicalPath $WebSSPRPortalPath -ApplicationPool $IISAppPoolName -Force
 
        # IIS Website performance tunning.

        #Ensure Application Initialization is available
        $webAppInit = Get-WindowsFeature -Name "Web-AppInit"

        if(!$webAppInit.Installed) 
        {
            Write-Host "$($webAppInit.DisplayName) not present, installing"
            Install-WindowsFeature $webAppInit -ErrorAction Stop
            Write-Host "`nInstalled $($webAppInit.DisplayName)`n" -ForegroundColor Green
        } else {Write-Host "$($webAppInit.DisplayName) was already installed" -ForegroundColor Yellow}

        #Fetch the site
        $site = Get-Website -Name $siteName

        if(!$site)
        {
            Write-Host "Site $siteName could not be found, exiting!" -ForegroundColor Yellow
            Break
        }


        #Fetch the application pool
        $appPool = Get-ChildItem IIS:\AppPools\ | Where-Object { $_.Name -eq $site.applicationPool }              

        #Set up .NET CLR version to be No Managed Code
        if($appPool.startMode -ne "AlwaysRunning")
        {
            Write-Host ".NET CLR version is set to $($appPool.managedRuntimeVersion), changing to 'No Managed Code'"
    
            $appPool | Set-ItemProperty -name "managedRuntimeVersion" -Value ""
            $appPool = Get-ChildItem IIS:\AppPools\ | Where-Object { $_.Name -eq $site.applicationPool }

            Write-Host ".NET CLR version is now set to 'No Managed Code'`n" -ForegroundColor Green
        } else {Write-Host ".NET CLR version was already set to 'No Managed Code' for the application pool $($site.applicationPool)" -ForegroundColor Yellow}        


        #Set up AlwaysRunning
        if($appPool.startMode -ne "AlwaysRunning")
        {
            Write-Host "startMode is set to $($appPool.startMode ), activating AlwaysRunning"
    
            $appPool | Set-ItemProperty -name "startMode" -Value "AlwaysRunning"
            $appPool = Get-ChildItem IIS:\AppPools\ | Where-Object { $_.Name -eq $site.applicationPool }

            Write-Host "startMode is now set to $($appPool.startMode)`n" -ForegroundColor Green
        } else {Write-Host "startMode was already set to $($appPool.startMode) for the application pool $($site.applicationPool)" -ForegroundColor Yellow}


        #Enable preloadEnabled on the IIS Site instance

        if(!(Get-ItemProperty "IIS:\Sites\$siteName" -Name applicationDefaults.preloadEnabled).Value) 
        {
            Write-Host "preloadEnabled is inactive, activating"
    
            Set-ItemProperty "IIS:\Sites\$siteName" -Name applicationDefaults.preloadEnabled -Value True
    
            Write-Host "preloadEnabled is now set to $((Get-ItemProperty "IIS:\Sites\$siteName" -Name applicationDefaults.preloadEnabled).Value)" -ForegroundColor Green
        } else {Write-Host "preloadEnabled already active" -ForegroundColor Yellow}

        #Apply connection string into appsettings.json
        
        $AppConfig = (Get-Content -path ($WebSSPRPortalPath + "appsettings.json")) -replace '^\s*//.*' | Out-String | ConvertFrom-Json
             
        if($sqlintegratedsecurity) {$AppConfig.ConnectionStrings.DefaultConnection = "Server=" + $sqlserver + ";Database=" + $sqldbname + ";integrated security=true;user id=;password="}
        else {$AppConfig.ConnectionStrings.DefaultConnection = "Server=" + $sqlserver + ";Database=" + $sqldbname + ";integrated security=false;user id=" + $sqluseraccount + ";password=" + $sqluserpassword}
       
        $AppConfig | ConvertTo-Json | set-content -Path ($WebSSPRPortalPath + "appsettings.json")
         
        #Start IIS website and app pool.
        if ((Get-IISAppPool -Name $IISAppPoolName).state -eq "Started") {write-host "$IISAppPoolName app pool is running."} else {Start-WebAppPool -Name $IISAppPoolName ; write-host "$IISAppPoolName app pool has been started."}
        if ((Get-Website -Name $siteName).state -eq "Started") {write-host "$siteName website is running."} else {Start-Website $siteName ; write-host "$siteName website has been started."}
        
        # Verify your SSPR Portal service by navigating into following URLs using your browser:
        write-host "Installation of Site: $siteName ($WebSSPRPortalPath) complete. App Pool Service account: $websspr_service_account" -ForegroundColor Green
        write-host "SSPR Portal service endpoints for access:" -ForegroundColor Green
        write-host "http://$serverFQDN/" -ForegroundColor Cyan
        
    }
    catch
    {
        Write-Host "$($MyInvocation.InvocationName): $_" -ForegroundColor red
    }    


}