ASP.Net Core .net 6 Demo Authentication Project using local Casdoor Docker Container on Windows Subsystem for Linux

I wanted to create a demo application with kind of real world authentication that I can easily adopt in projects.

This was not so easy as I thought.

1st: I wanted an identity provider that offers OAuth2.

2nd: I wanted the identity provider on my own machine or at least within my infrastructure.

3rd: I wanted a setup that I can describe to reproduce it.

Here is the result. – I work on Windows 11. It should work with Windows 10 too. – I use Visual Studio 2022.

Part 1: Preparation

Install Windows Subsystem for Linux version 2:

https://docs.microsoft.com/en-us/windows/wsl/install

Install Docker Desktop for Windows

https://docs.docker.com/desktop/windows/install/

Ensure you enabled WSL2 support in Docker!

Part 2: Setup

I need Casdoor as identity provider. It’s open source:

https://casdoor.org/

Pull the latest “all-in-one” Docker image of Casdoor.

docker pull casbin/casdoor-all-in-one

Now you create the container…

docker run -d -p  8000:8000 --name casdoor -v ./casdoor-data:/var/lib/mysql casbin/casdoor-all-in-one

Three comments on that:

  1. The Casdoor portal on your machine can be accessed using http://localhost:8000. If you need another port that change “8000:8000” to something else like “9000:8000”. The second port is internally used inside the Docker container. Do not change that. The first port is the published one on your machine.
  2. Casdoor is an identity provider. You will need to create identities in it. Of course you do not want to do that again and again. Therefore it’s a good idea to put the data of the Casdoor container into Docker volume. If you later recreate the container the volume will remain on disk.
  3. You ask: “Where is the Docker volume located on disk on WSL 2”? Good question! WSL creates a hidden mount point (?) that you can access on Windows by accessing \\wsl$ in the Explorer. Then you can navigate to the correct folder that contains the volume of Casdor: \\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\casdoor-data

Now you can open Casdoor:

http://localhost:8000

The default login is: username “admin” with password “123” (without “”)

Now … create some users.

Then… create an application:

Part 3: The code

Now clone my github project:

https://github.com/ikarstein/com.kenaro.public.OAuth2Demo.Casdoor

Open the solution in Visual Studio 2022. Run it.

It will open a browser and looks like this:

Click “Authenticate using Casdoor”

Authenticate…

Thats it.

The magic happens in “Startup.cs”

.AddOAuth("casdoor", "Casdoor", options =>
        {
            options.AuthorizationEndpoint = "http://localhost:8000/login/oauth/authorize";
            options.TokenEndpoint = "http://localhost:8000/api/login/oauth/access_token";
            options.UserInformationEndpoint = "http://localhost:8000/api/userinfo";
            options.ClientId = "dc6556419364997a4032";
            options.ClientSecret = "2a4dbd07bbb655777a928ef99039a11d1e81d9d4";
            options.CallbackPath = "/signin-casdoor";
            options.ClaimsIssuer = "iss";
            options.SaveTokens = true;
            options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "name");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.Gender, "data", "gender");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.Name, "data", "displayName");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.Email, "data", "email");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.HomePhone, "data", "phone");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.Locality, "data", "location");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.Webpage, "data", "homepage");
            options.ClaimActions.MapJsonSubKey(ClaimTypes.Role, "data", "type");

            options.Events.OnCreatingTicket = async creatingTicketContext =>
            {
                var token = creatingTicketContext.Properties?.GetString(".Token.access_token");

                using var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:8000/api/get-account");
                request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

                using var response = await creatingTicketContext.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, creatingTicketContext.HttpContext.RequestAborted);
                if (!response.IsSuccessStatusCode)
                {
                    throw new HttpRequestException("An error occurred while retrieving the user profile from Authentik.");
                }

                var userInfo = await response.Content.ReadAsStringAsync(creatingTicketContext.HttpContext.RequestAborted);
                using var jsonDoc = JsonDocument.Parse(userInfo);
               
                creatingTicketContext.RunClaimActions(jsonDoc.RootElement);
            };
        });

I took a while to figure out how to configure the OAuth2 provider.

Manager Attribute not Updated in AD Import for Profiles on SharePoint Server On-Premises

Today I could solve a wired problem. It belongs to User Profile “AD Import” on SharePoint Server 2019.

The profile overview of a person stated two different information: At the top there was the correct department but in the organizational chart the person had the wrong manager and so a wrong department information.

The organizational chart is generated from the “Manager” attribute of the SharePoint user profile.

In the profile I saw a correct “department” attribute value but a wrong “manager” attribute value.

I started a “Full AD Import”.

In the SharePoint log I saw an exception with the message, that a XML file in the SharePoint timer cache on one server could not be changed.

So first I refreshed all timer cache folders on all servers: Stop Windows service “sptimerv4” on every server. Find the subfolder with file “cache.ini” below c:\programdata\microsoft\sharepoint\config. The folder has a GUID as name. Than delete all file but cache.ini. Than set the content of cache.ini to “1” (without “”) and restart the service “sptimerv4”. The cache folder gets filled again. The content of “cache.ini” will be set to a valid value…

Next I restarted AD Import using PowerShell:

$s = get-spserviceapplication <guid>
$s.StartImport($true)

Now I saw the sync working in the SharePoint log but the manager attribte was not updated still.

My next idea was that maybe the property mapping had a problem. I looked at the mapping config but it looked OK in the editor.

Out of curiosity I have removed the assignment for “manager”. Than I ran the import and checked the user profile afterwards.

As a big suprise for me the manager attribte now got updated. WITHOUT MAPPING.

It seems that the manager attribute is mapped internally.

kenaflow – Enterprise Business Automation with PowerShell

Almost exactly four years ago I started developing the software kenaflow. The project started as a workflow engine for SharePoint. Meanwhile the software can do a lot more, e.g. workflows for email inboxes.

Workflows with kenaflow work via PowerShell scripts: The data to be processed is passed individually to one or more scripts. What the developer does there is his/her business.

kenaflow creates the framework and takes care of the execution (scheduling).

kenaflow provides HTTPS endpoints, e.g. for web hooks or SharePoint remote events.

The whole thing is mega flexible and scalable.

Interested? Then feel free to send me a message! – kenaflow is free for the first six months in the full version!

The latest offering is kenaflow as a hosted cloud service. We provide kenaflow on a dedicated virtual machine per customer. You develop workflow scripts via Visual Studio Code Remote – using SSH. – Even on the smallest virtual machine you can run an unlimited number of workflows, limited only by the computational intensity and execution frequency of your workflows.

“Access Denied” When Trying to Save a Web as Template in SP 2019 On-Premises

My customer tries to save a sub web “as template” using “/_layouts/15/savetmpl.aspx“.

He gets an “Access Denied” (“This site has not been shared with you”) page.

It took me a while to understand the ULS (SharePoint log). Finally I found the related lines.

Requiring ACPRight

The permission is being checked for a write operation

Requiring ManageListsRight and ACPRight as this is a write operation on catalog

Permission check failed. Asking for 0x00040802, have 0x7FFFFFFFFFFBFFFF

SPRequest.PutFile: UserPrincipalName=i:0).w|s-1-5-21-45689356773-24675323-1484686, AppPrincipalName= ,bstrUrl=https://sharepoint.farm ,bstrWebRelativeUrl=_catalogs/solutions/ikarstein.wsp ,cbFile=11111 ,punkSPFileMgr=<null> ,punkFFM=<null>

System.UnauthorizedAccessException: Access denied., StackTrace:   
 at Microsoft.SharePoint.SPFileCollection.AddStreamOrBytesInternal(String urlOfFile, Stream file, Int64 fileSizeToSave, SPFileStreamManager spmgr, Int64 fileOpt, String createdBy, String modifiedBy, Int32 createdByID, Int32 modifiedByID, DateTime timeCreated, DateTime timeLastModified, Object varProperties, String checkInComment, Stream formatMetadata, String lockIdMatch, String etagToMatch, SPLockType lockType, String lockId, TimeSpan lockTimeout, Boolean validateRequiredFields, Guid bitsSessionId, Guid originatorId, SPVirusCheckStatus& virusCheckStatus, String& virusCheckMessage, String& etagNew, Boolean& ignoredRequiredProps, SPFileInfo& fileProps)    
 at Microsoft.SharePoint.SPFileCollection.Add(String urlOfFile, Stream file, Hashtable properties, Boolean overwrite, Boolean requireWebFilePermissions)    
 at Microsoft.SharePoint.SPSolutionExporter.ExportWebToGallery(SPWeb web, String solutionFileName, String title, String description, ExportMode exportMode, Boolean includeContent, String workflowTemplateName, String destinationListUrl, Action`1 solutionPostProcessor, Boolean activateSolution)    
 at Microsoft.SharePoint.ApplicationPages.SaveAsTemplatePage.BtnSaveAsTemplate_Click(Object sender, EventArgs e)    
 at System.Web.UI.WebControls.Button.OnClick(EventArgs e)    
 at System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)    
 at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)    
 at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)    
 at System.Web.UI.Page.ProcessRequest()    
 at System.Web.UI.Page.ProcessRequest(HttpContext context)    
 at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()    
 at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)    
 at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    
 at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error)    
 at System.Web.HttpApplication.BeginProcessRequestNotification(HttpContext context, AsyncCallback cb)    
 at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)    
 at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
 at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
 at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)    
 at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)    
 at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)    
 at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)

Access Denied. Exception: Access denied., StackTrace:  
 at Microsoft.SharePoint.Library.SPRequestInternalClass.PutFile(String bstrUrl, String bstrWebRelativeUrl, Object punkFile, Int64 cbFile, Object punkSPFileMgr, Object punkFFM, SPFileSaveParams sfsp, SPFileInfo& pFileProps, UInt32& pdwVirusCheckStatus, String& pVirusCheckMessage, String& pEtagReturn, Byte& piLevel, Int32& pbIgnoredReqProps)    
 at Microsoft.SharePoint.Library.SPRequest.PutFile(String bstrUrl, String bstrWebRelativeUrl, Object punkFile, Int64 cbFile, Object punkSPFileMgr, Object punkFFM, SPFileSaveParams sfsp, SPFileInfo& pFileProps, UInt32& pdwVirusCheckStatus, String& pVirusCheckMessage, String& pEtagReturn, Byte& piLevel, Int32& pbIgnoredReqProps).

It took me a lot of time to figure that out…

$w = Get-SPWeb "https://sharepoint.farm"

$w.site.DenyPermissionsMask

([long]$w.site.DenyPermissionsMask).ToString("x")

0x7FFFFFFFFFFBFFFFL

0x7FFFFFFFFFFFFFFFL

0x7FFFFFFFFFFFFFFFL -bxor 0x40000

The line ” $w.site.DenyPermissionsMask ” returned the result: “AddAndCustomizePages” => A (A) C P => ACP ? => ACPRight … ?? There it was !!!

Hex: 0x40000 (line ([long]$w.site.DenyPermissionsMask).ToString("x") )

The long hex values showed me that exactly that right was for some reasons “denied” on the site collection.

I solved it using this command:

$w.site.DenyPermissionsMask = [microsoft.sharepoint.spbasepermissions]::EmptyMask

The previous setting can be restored with:

$w.site.DenyPermissionsMask =[microsoft.sharepoint.spbasepermissions]::AddAndCustomizePages

After the fix, I checked all site collections for this particular setting:

get-spsite -limit all | % {
    if( ($_.DenyPermissionsMask) -ne "EmptyMask") {
        write-host $_.url  -ForegroundColor red
        write-host "`t" ($_.DenyPermissionsMask) -ForegroundColor red
    } else {
        #write-host $_.url  -ForegroundColor green
    }
}

… No other site collection has had a setting other than “EmptyMask”.

SQL Execution in Powershell: Write Server Messages to Console

It has been a problem for me to work with SQL backup and restore for SharePoint with Powershell…

Issuing the commands is no problem. A simple SQL Server connection is required.

But for long running tasks I need to see the server messages in Powershell.

Here is a trick to do that. I use it since years. As a C# developer I know the “event handler” that can receive the messages…

$cnn = New-Object System.Data.SqlClient.SqlConnection

$cnn.add_InfoMessage([System.Data.SqlClient.SqlInfoMessageEventHandler] {
  param($sender, $event);

  Write-Host $event.Message;
}); 

$cnn.FireInfoMessageEventOnUserErrors = $true

$cnn.ConnectionString = "Server=sqlserver;Database=master;Integrated Security=True;Connection Timeout=86400"

$cnn.Open()

“add_InfoMessage” does the trick.

Cleanup IIS Log using PowerShell

On every SharePoint or IIS server I need the following script:

Short Version, e.g. for Windows Task Scheduler

powershell -command “get-childitem -path c:\inetpub\logs\logfiles -filter *.log -recurse | ? { $_.LastWriteTimeUtc -lt [datetime]::utcnow.addmonths(-2)} | remove-item -force -confirm:$false -whatif”

(You may need to adjust the path to the log files!)

Long Version

cls

$path = "C:\inetpub\logs"
 
$dt = [datetime]::Now

$s = [long]0
Get-ChildItem "$($path)\LogFiles" -Filter "*.log" -Recurse | % { $s += $_.Length }

Write-host "Size before: $($s / 1024 / 1024) MB"

$s = [long]0
Get-ChildItem "$($path)\LogFiles" -Filter "*.log" -Recurse | ? { ($dt - $_.LastWriteTime).TotalDays -gt 30 } | % { $s += $_.Length; $_ | Remove-Item -Confirm:$false -Force }

Write-host "Removed: $($s / 1024 / 1024) MB"

$s = [long]0
Get-ChildItem "$($path)\LogFiles" -Filter "*.log" -Recurse | % { $s += $_.Length }

Write-host "Size after: $($s / 1024 / 1024) MB"

New-SPConfigurationDatabase – Error “Given key was not present in the directory”

During configuration of a new SharePoint 2019 farm I got an error in PowerShell Cmdlet New-SPConfigurationDatabase.

The given key was not present in the directory.

I found some hints… E.g.: http://alstechtips.blogspot.com/2014/04/sharepoint-2013-given-key-was-not.html

I checked the farm account used with the Cmdlet.

I found that the farm account did not have “Read” permission for “All Authenticated Users”.

After granting this permission the Cmdlet succeeded.

Set TLS Version in PowerShell

It has been described lots of times before. It’s just for the records…

This code sets the used TLS version for secure HTTP connections to version 1.2

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Here are the valid values:

SystemDefault
Ssl3
Tls
Tls11
Tls12

The latests supported version depends on your .NET Framework. On Windows 10 20H2 the setting Tls13 will work too.

With this code you can list the valid values:

[Net.SecurityProtocolType]::GetNames([Net.SecurityProtocolType])

Create Self Signed Certificate with PowerShell on Windows Server 2012 R2.

A little bit outdated. I know.

But customers not always have the latest server versions running.

Today a customer asked me how to create a self signed SSL certificate with “subject alternate names” (SAN) using PowerShell on Windows Server 2012 R2.

The default cmdlet “New-SelfSignedCertificate” has not all features on this server OS.

The customer found a script but it did not offer SAN. I found another one (in german language) that gave me the other information…

Sources:

https://docs.microsoft.com/en-us/archive/blogs/vishalagarwal/generating-a-certificate-self-signed-using-powershell-and-certenroll-interfaces

https://www.symplasson.de/it-blog/san-erweiterung-csr-windows-ca

I assembled both scripts. Here is the result.

# script assembled from these sources
#  - https://docs.microsoft.com/en-us/archive/blogs/vishalagarwal/generating-a-certificate-self-signed-using-powershell-and-certenroll-interfaces
#  - https://www.symplasson.de/it-blog/san-erweiterung-csr-windows-ca
# …by Ingo Karstein ( ik a.t. kenaro.com)
#
# Useful e.g. on Windows Server 2012R2 because there are less functionality in cmdlet New-SelfSignedCertificate.
#
# The SSL cert ist written to certstore "My" of "LocalMachine"

 $name = new-object -com "X509Enrollment.CX500DistinguishedName.1"
 $name.Encode("CN=srv", 0)

 $key = new-object -com "X509Enrollment.CX509PrivateKey.1"
 $key.ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
 $key.KeySpec = 1
 $key.Length = 4096
 $key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)"
 $key.MachineContext = 1
 $key.Create()

 $serverauthoid = new-object -com "X509Enrollment.CObjectId.1"
 $serverauthoid.InitializeFromValue("1.3.6.1.5.5.7.3.1")

 $AlternativeNames=@("srv", "localhost")
 $AlternativeIPs=@("127.0.0.1", "::1")

 $SAN = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames
 $IANs = New-Object -ComObject X509Enrollment.CAlternativeNames

 foreach ($SANstr in $AlternativeNames)
 {
     $IAN = New-Object -ComObject X509Enrollment.CAlternativeName
     $IAN.InitializeFromString(0x3,$SANstr)
     $IANs.Add($IAN)
 }

 foreach ($SANstr in $AlternativeIPs)
 {
     $IAN = New-Object -ComObject X509Enrollment.CAlternativeName
     $IAN.InitializeFromString(0x3,$SANstr)
     $IANs.Add($IAN)
     $IAI = New-Object -ComObject X509Enrollment.CAlternativeName    
     $IAI.InitializeFromRawData(8, 0x1, 
        [Convert]::ToBase64String(
       ([System.Net.IpAddress] $SANstr).GetAddressBytes())) $IANs.Add($IAI)
 }

 $SAN.InitializeEncode($IANs)

 $ekuoids = new-object -com "X509Enrollment.CObjectIds.1"
 $ekuoids.add($serverauthoid)

 $ekuext = new-object -com "X509Enrollment.CX509ExtensionEnhancedKeyUsage.1"
 $ekuext.InitializeEncode($ekuoids)

 $cert = new-object -com "X509Enrollment.CX509CertificateRequestCertificate.1"
 $cert.InitializeFromPrivateKey(2, $key, "")
 $cert.Subject = $name
 $cert.Issuer = $cert.Subject
 $cert.NotBefore = (get-date).AddDays(-1)
 $cert.NotAfter = $cert.NotBefore.Addyears(50)
 $cert.X509Extensions.Add($ekuext)
 $cert.X509Extensions.Add($SAN)
 $cert.Encode()

 $enrollment = new-object -com "X509Enrollment.CX509Enrollment.1"
 $enrollment.InitializeFromRequest($cert)

 $certdata = $enrollment.CreateRequest(0)

 $enrollment.InstallResponse(2, $certdata, 0, "")