Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: 2
updates:
# NuGet dependencies — the provider's packages.config (Newtonsoft.Json) and
# the test project's PackageReferences (WireMock.Net, MSTest, etc.).
- package-ecosystem: nuget
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
test-dependencies:
patterns:
- "MSTest*"
- "Microsoft.NET.Test.Sdk"
- "coverlet*"
- "WireMock.Net"

# Keep the GitHub Actions used by the CI and CodeQL workflows up to date.
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

# PR test gate. Builds the test project (which transitively builds the .NET
# Framework 4.8 provider DLL) and runs the test suite on a Windows runner.
#
# Notes / constraints (see also the README + project notes):
# * The provider targets .NET Framework 4.8 and strong-name signs with
# adfskeyfile.pfx, which is intentionally NOT in the repo. CI therefore
# builds with /p:SignAssembly=false so no key is needed; signing only
# matters for shipped artifacts, not for running tests.
# * Must use full MSBuild (from VS) — `dotnet build`/`dotnet test` reject the
# PFX-signing csproj, and the provider is a classic (non-SDK) project.
# * The test project is net8.0 and references the net48 provider DLL, so the
# job runs on windows-latest and uses vstest.console on the built assembly,
# mirroring the known-good local flow.

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: windows-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2

- name: Restore (PackageReference + packages.config)
# SolutionDir is required for RestorePackagesConfig (it restores the
# provider's packages.config). We restore via the test project rather
# than the .sln to avoid evaluating the WiX installer project, whose
# targets aren't installed on the runner.
run: msbuild Tests\Tests.csproj /t:Restore /p:RestorePackagesConfig=true /p:SolutionDir=${{ github.workspace }}\ /v:minimal /nologo

- name: Build tests (unsigned)
run: msbuild Tests\Tests.csproj /p:Configuration=Debug /p:SignAssembly=false /v:minimal /nologo

- name: Run tests
shell: pwsh
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vstest = & $vswhere -latest -find "**\vstest.console.exe" | Select-Object -First 1
if (-not $vstest) { throw "vstest.console.exe not found" }
& $vstest "Tests\bin\Debug\net8.0\Tests.dll" --logger:"trx;LogFileName=test-results.trx" --ResultsDirectory:TestResults

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: TestResults/*.trx
if-no-files-found: ignore
54 changes: 54 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CodeQL

# Static analysis (SAST) of the provider with GitHub CodeQL.
#
# Uses manual build mode: CodeQL analyzes the *compiled* provider assembly, so
# dataflow/taint analysis runs at full precision (worth it for auth code that
# touches credentials, DPAPI, registry ACLs and WebAuthn payloads). We build the
# provider project on its own — not the test project — so results cover shipped
# code only, not test fixtures.
#
# The build mirrors the CI gate's trick: /p:SignAssembly=false so the gitignored
# adfskeyfile.pfx isn't needed. Findings appear in the repo's Security tab and as
# PR annotations. Note: this is a PUBLIC repo, so those findings are public too.

on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 6 * * 1' # weekly, Monday 06:00 UTC — catches new CodeQL rules
workflow_dispatch:

jobs:
analyze:
name: Analyze (csharp)
runs-on: windows-latest
permissions:
security-events: write # upload SARIF to the Security tab
contents: read
actions: read

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v2

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
build-mode: manual
queries: security-extended

- name: Build provider (unsigned)
run: |
msbuild privacyIDEAADFSProvider\privacyIDEAADFSProvider.csproj /t:Restore /p:RestorePackagesConfig=true /p:SolutionDir=${{ github.workspace }}\ /v:minimal /nologo
msbuild privacyIDEAADFSProvider\privacyIDEAADFSProvider.csproj /p:Configuration=Release /p:SignAssembly=false /v:minimal /nologo

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:csharp"
22 changes: 14 additions & 8 deletions ADFSInstaller/ADFSInstaller.wixproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,18 @@
<PropertyGroup>
<PostBuildEvent />
</PropertyGroup>
<!--
To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Wix.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<!-- Append the provider's assembly version to the built MSI filename, e.g.
PrivacyIDEA-ADFS-Provider-v1.4.0.0.msi. The version is read from the freshly built provider DLL —
the same source as the MSI's bound ProductVersion (Include.wxi) — so it tracks AssemblyInfo.cs
automatically with no manual step. The provider is AnyCPU, so it always builds to bin\$(Configuration). -->
<Target Name="AppendVersionToMsiName" AfterTargets="Build" Condition="Exists('$(TargetPath)')">
<GetAssemblyIdentity AssemblyFiles="..\privacyIDEAADFSProvider\bin\$(Configuration)\privacyIDEA-ADFSProvider.dll">
<Output TaskParameter="Assemblies" ItemName="_ProviderAssembly" />
</GetAssemblyIdentity>
<PropertyGroup>
<_VersionedMsiPath>$(TargetDir)$(OutputName)-v%(_ProviderAssembly.Version).msi</_VersionedMsiPath>
</PropertyGroup>
<Move SourceFiles="$(TargetPath)" DestinationFiles="$(_VersionedMsiPath)" />
<Message Importance="high" Text="Installer output renamed to $([System.IO.Path]::GetFileName('$(_VersionedMsiPath)'))" />
</Target>
</Project>
24 changes: 22 additions & 2 deletions ADFSInstaller/Dialogues.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@
<Condition Action="enable">TRIGGERCHALLENGES=1</Condition>
<Condition Action="enable">ENABLEENROLL=1</Condition>
</Control>
<Control Type="Edit" Id="in_servicepass" Width="78" Height="15" X="170" Y="178" Text="Password" Property="SERVICEPASS" Help="Service password" ToolTip="Password of the service account.">
<!-- ToolTip is kept short: it maps to the MSI Control.Help column, which is capped at 50
chars (ICE03). The full explanation is behind the "?" info button (btn_pass_info). -->
<Control Type="Edit" Id="in_servicepass" Width="78" Height="15" X="170" Y="178" Password="yes" Text="Password" Property="SERVICEPASS" ToolTip="Stored encrypted (enc: prefix).">
<Condition Action="disable">TRIGGERCHALLENGES&lt;&gt;1</Condition>
<Condition Action="enable">TRIGGERCHALLENGES=1</Condition>
<Condition Action="enable">ENABLEENROLL=1</Condition>
</Control>
<!-- Small info button next to the password field. MSI cannot show a long hover tooltip
(the Help column is ~50 chars), so clicking it opens PasswordEncryptionInfoDlg with the
full explanation. This frees the dialog space the old inline note consumed. -->
<Control Type="PushButton" Id="btn_pass_info" Width="16" Height="16" X="250" Y="177" Text="?" ToolTip="Password storage info (click)">
<Publish Event="SpawnDialog" Value="PasswordEncryptionInfoDlg">1</Publish>
</Control>
<!-- END TRIGGER CHALLENGE / SEND PASSWORD CONTROLS -->
<Control Type="CheckBox" Id="cbox_upn" Width="50" Height="17" X="38" Y="135" Text="Use UPN" CheckBoxValue="1" Property="USEUPN" />
<Control Type="CheckBox" Id="cbox_debuglog" Width="116" Height="17" X="38" Y="214" Text="Activate debug log" CheckBoxValue="1" Property="DEBUGLOG" />
Expand All @@ -43,7 +51,10 @@
</Control>
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="&amp;Next">
<Publish Event="NewDialog" Value="VerifyReadyDlg">URL</Publish>
<Publish Event="SpawnDialog" Value="NoServerURLMessageDlg">URL="Server URL"</Publish>
<!-- URL is mandatory: if the property is empty, show the hint dialog instead of
silently doing nothing. (The previous condition compared against a literal
"Server URL" that the field never contained, so neither publish fired.) -->
<Publish Event="SpawnDialog" Value="NoServerURLMessageDlg">NOT URL</Publish>
</Control>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
<Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
Expand Down Expand Up @@ -71,6 +82,15 @@
</Text>
</Control>
</Dialog>
<!-- Full explanation behind the "?" info button on the password field. -->
<Dialog Id="PasswordEncryptionInfoDlg" Width="280" Height="150" Title="[ProductName] Setup" NoMinimize="yes">
<Control Id="InfoText" Type="Text" X="15" Y="15" Width="250" Height="95" TabSkip="no" NoPrefix="yes">
<Text>The service account password is stored encrypted (an "enc:" prefix is added to the registry value). To change it later, enter a new cleartext value here, or write it into the registry without the prefix - the provider encrypts it on the next AD FS restart.</Text>
</Control>
<Control Id="OK" Type="PushButton" X="112" Y="120" Width="56" Height="17" Default="yes" Cancel="yes" Text="&amp;OK">
<Publish Event="EndDialog" Value="Return">1</Publish>
</Control>
</Dialog>
</UI>
</Fragment>
</Wix>
9 changes: 9 additions & 0 deletions ADFSInstaller/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@

<Property Id="ARPURLINFOABOUT" Value="$(var.AppURLInfoAbout)"/>
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" />
<!-- Modify is hidden. The maintenance "Change" flow is NOT safe with the current custom-action
conditions: RunPowerShellUninstall fires on `Installed` (true during a Change) while
RunPowerShellInstall is gated on `NOT Installed` (skipped during a Change), so a Change would
run Uninstall.ps1 (unregister + GAC remove) without re-registering — tearing the provider out
of AD FS. Fixing that needs the uninstall CAs gated on REMOVE="ALL" (with attention to when
REMOVE is populated relative to CostFinalize) AND making the config values actually re-write on
a Change, all verified against the install/change/repair/upgrade/uninstall matrix. Until then,
do not expose Modify. Change the service password via the registry (the provider re-encrypts it
on the next read) or a full reinstall. -->
<Property Id="ARPNOMODIFY" Value="yes" Secure="yes" />

<Condition Message="You need to be an administrator to install this product.">Privileged</Condition>
Expand Down
17 changes: 17 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 2026-06-03 v1.4.0

Support for privacyIDEA 3.13 features:
* push_code_to_phone
* enroll_via_multichallenge_optional
* enroll_via_multichallenge for smartphone containers

Secret handling:
* The `service_pass` is now encrypted at rest with Windows DPAPI. Existing plaintext values (and values typed directly into the registry) are migrated to encrypted storage automatically the first time the provider reads them, with a one-time warning written to the event log.
* The installer hardens the ACL on the configuration registry key so the `service_pass` is no longer readable by non-admin users on the machine.

Other changes:
* Added more German-speaking LCIDs (de-AT, de-CH, de-LI, de-LU).
* Updated the event log location this application writes to. It now writes to the general Windows **Application** log with the source "privacyIDEAProvider" (if an earlier version registered the source under a different log, it is moved to Application automatically). The event source is removed on uninstall.
* The installer now checks for .NET Framework 4.8 and aborts with instructions if it is missing.
* The service password field in the installer is masked.

## 2025-06-02 v1.3.0

**Use this version only with privacyIDEA 3.11 or higher**
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
This project builds on Stephan Traub's [original provider v1.3.8.2](https://github.com/sbidy/privacyIDEA-ADFSProvider/tree/f66100713e650d134ac50fcbd3965b71ae588d47).

## Requirements
To use the provider, the [.NET Framework 4.8](https://dotnet.microsoft.com/download/dotnet-framework/net48) is required on the target machine.
The [.NET Framework 4.8](https://dotnet.microsoft.com/download/dotnet-framework/net48) is required on the target machine. It is included in-box on Windows Server 2022 and newer, but **not** on Server 2016 (ships with 4.6.2) or Server 2019 (ships with 4.7.2) — installing the ADFS role does not add it. On those versions install .NET Framework 4.8 first (it is also commonly delivered via Windows Update). `Install.ps1` checks for it and aborts with instructions if it is missing.

## Configuration
Starting with v1.3.0, this provider *can* be used as primary authentication method in ADFS. However, ADFS will inject a form to request the username before this provider, even if it is configured as primary, which makes the experience of passkey usernameless authentication not great in ADFS.
Expand All @@ -12,6 +12,12 @@ You could then choose to add "Forms Authentication" as additional, to have the p
The provider is configured using the registry. The keys are located at `HKEY_LOCAL_MACHINE\SOFTWARE\NetKnights GmbH\PrivacyIDEA-ADFS`.
After changing the configuration, the AD FS Service has to be restarted for the changes to become active. This can be done using the PowerShell command `Restart-Service adfssrv`.

`Install.ps1` hardens the access control list on this key: by default `HKLM\SOFTWARE` grants the local *Users* group read access, which would let any non-admin on the machine read the `service_pass` in cleartext. After installation only `SYSTEM`, `Administrators`, and the AD FS service account can access the key. If you create the configuration key *after* running the installer, re-run `Install.ps1` so the ACL is applied.

The `service_pass` is additionally encrypted at rest using Windows DPAPI. You can enter it as plaintext (via the installer dialog or directly in the registry); the provider encrypts it in place the first time it reads it, replacing the value with an `enc:`-prefixed ciphertext. To change it later, simply enter a new plaintext value — it will be re-encrypted on the next read. This protects the password in registry exports and backups; it is *not* a boundary against a local administrator, who on an AD FS server already controls the token-signing key.

DPAPI encryption is bound to the machine, so an encrypted value cannot be decrypted on a different server (e.g. after a restore, clone, or migration). On a new machine — including each node of an AD FS farm — enter the password again and it will be re-encrypted locally. The provider logs an event if it finds an `enc:` value it cannot decrypt.

| Key Name | Explanation |
| ----- | ----- |
| url | The url of the privacyIDEA server. Has to include https://! |
Expand Down Expand Up @@ -44,7 +50,20 @@ Adding `"SchUseStrongCrypto"=dword:00000001` to `HKEY_LOCAL_MACHINE\SOFTWARE\Mic
and `HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft.NETFramework\v4.0.30319` fixes the problem.

## Event Log
Errors will be written to the Windows Event Log in the `AD FS/Admin` category. To get a more detailed log, activate the `debug_log` setting as explained in the next section.
Errors will be written to the Windows **Application** event log under the `privacyIDEAProvider` source. To get a more detailed log, activate the `debug_log` setting as explained in the next section.

## Logging and personal data
The detailed debug log (`debug_log`, written to `C:\PrivacyIDEA-ADFS log.txt`) is **off by default** and should only be enabled while troubleshooting. When enabled, it contains personal data and should be treated accordingly:

- **Usernames, UPNs and domains** are recorded so that an authentication can be traced end to end. This is required for the log to be useful for diagnostics and for security auditing.
- **Full server responses** from privacyIDEA are recorded as well; these may include token serials and any user attributes the server returns.
- **Secrets are masked.** The service account password (`password`), the user credential (`pass`, which in privacyIDEA carries the static PIN in front of the OTP) and the `Authorization` header (JWT) are redacted before anything is written. One-time values such as transaction IDs are not secrets and are logged in clear text.

This log lives on your own AD FS server and is under your control as the data controller. To meet your obligations (GDPR / ISO 27001):

- Keep `debug_log` disabled in normal operation and re-disable it once a problem is resolved.
- Restrict read access to the log file to administrators, and define a retention period after which it is deleted.
- Treat the file as in scope for your access-control, retention and audit-logging controls.

## Debugging
Errors in the provider can be found by looking at the Windows Event Log or activating the `debug_log` setting.
Expand Down
Loading
Loading