Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,403 @@
# Anmelden mit Microsoft — Technische Anleitung
## Überblick
Die Funktion "Anmelden mit Microsoft" nutzt **Microsoft Entra ID (Azure AD)** über **OpenID Connect** mit der **MSAL-Bibliothek**. Der Client holt ein ID-Token von Microsoft, schickt es an die c-entron API, die es validiert, den User per Entra Object ID (`oid`-Claim) nachschlägt und ein c-entron Session-Ticket zurückgibt.
---
## Kompletter Flow
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Client App │ │ c-entron API │ │ Microsoft │
│ │ │ (Web Service) │ │ Entra ID │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
1. GET /config/jwt ─────────>│ │
│<── { Authority, │ │
│ Audience, │ │
│ Enabled } │ │
│ │ │
2. MSAL: Token von Microsoft holen ──────────────────> │
│<──────────────────────────── AuthenticationResult│
│ (enthält IdToken) │ │
│ │ │
3. POST /jwt/login ─────────>│ │
│ Authorization: │ │
│ Bearer {IdToken} │ 4. JWT Middleware │
│ Body: { Application, │ validiert Token │
│ AppVersion, │ (Signatur, Issuer, │
│ Device } │ Audience, Lifetime)│
│ │ │
│ │ 5. oid-Claim extrahieren│
│ │ → User in DB suchen │
│ │ (OpenIdConnect- │
│ │ SubjectIdentifier) │
│ │ │
│ │ 6. Ticket erstellen │
│<── "ticket-string" │ │
│ │ │
7. Ticket für alle │ │
weiteren API-Calls nutzen │ │
```
---
## Schritt 1: JWT-Konfiguration abrufen
Anonymer Endpoint — kein Auth nötig.
```http
GET {baseUrl}/config/jwt
```
**Response:**
```json
{
"Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"Audience": "{azure-ad-client-id}",
"Enabled": true
}
```
| Feld | Bedeutung |
|---|---|
| `Authority` | OpenID Connect Authority URL (Entra ID Tenant) |
| `Audience` | Azure AD Application (Client) ID |
| `Enabled` | `true` wenn beide Werte konfiguriert sind |
Wenn `Enabled == false` → OIDC ist nicht konfiguriert, Abbruch.
---
## Schritt 2: ID-Token von Microsoft Entra ID holen (MSAL)
Mit den Werten aus Schritt 1 wird ein MSAL Public Client konfiguriert:
- **Client ID** = `Audience` aus der JWT-Konfiguration
- **Authority** = `Authority` aus der JWT-Konfiguration
- **Scopes** = `["openid", "profile"]`
- **Broker** = Windows WAM (optional, für SSO mit Windows-Anmeldung)
**Ergebnis:** Ein `AuthenticationResult` mit einem `IdToken` (JWT).
> **Wichtig:** Es wird das **ID-Token** verwendet, nicht das Access-Token. Die Scopes `openid` und `profile` reichen aus.
---
## Schritt 3: ID-Token gegen c-entron Ticket tauschen
```http
POST {baseUrl}/jwt/login
Authorization: Bearer {microsoft-id-token}
Content-Type: application/json
{
"Application": "{verschlüsselte-lizenz-GUID}",
"AppVersion": "2.1.2605.636",
"Device": "MACHINE-NAME"
}
```
| Feld | Typ | Bedeutung |
|---|---|---|
| `Application` | string | Verschlüsselte Lizenz-GUID der Anwendung |
| `AppVersion` | string | Version der Client-Anwendung |
| `Device` | string | Gerätename (`Environment.MachineName`) |
**Response (Erfolg):** `200 OK`
```
ticket-hash-string
```
Der Response-Body enthält direkt den Ticket-String (plain text, kein JSON).
**Response (Fehler):** `400 Bad Request` oder `401 Unauthorized`
---
## Schritt 4: Ticket für weitere API-Calls verwenden
Das erhaltene Ticket wird für alle weiteren c-entron API-Aufrufe als Authentifizierung verwendet.
---
## Was auf dem Server passiert
### JWT-Validierung (Middleware)
Die ASP.NET Core JWT Bearer Middleware:
1. Lädt das OpenID Connect Discovery Document von `{Authority}/.well-known/openid-configuration`
2. Holt die Signing Keys vom JWKS-Endpoint
3. Validiert: Signatur, Issuer, Audience, Lifetime
4. Befüllt `HttpContext.User` mit den Claims
### User-Lookup
```csharp
// oid-Claim = Microsoft Entra Object ID
var oid = identity.Claims.FirstOrDefault(c => c.Type == "oid")?.Value;
// User in der Datenbank suchen
var user = dao.GetEntity(where => where.OpenIdConnectSubjectIdentifier == oid);
```
Die Spalte `OpenIdConnectSubjectIdentifier` in der Tabelle `Sichbenu` (AppUser) enthält die Microsoft Entra Object ID des verknüpften Benutzers.
### Ticket-Erstellung
```csharp
var salt = CryptoUtils.CreateSalt(32);
var ticketId = CryptoUtils.CreatePasswordHash(deviceId, salt); // SHA-basierter Hash
var expireDate = DateTime.Now.AddMinutes(30); // 30 Min Gültigkeit
// INSERT INTO Ticket (TicketId, ExpiryDate, ApplicationID, LicenseGUID, UserI3D, DeviceId)
```
---
## Voraussetzungen
### Azure AD App Registration
| Einstellung | Wert |
|---|---|
| Application (Client) ID | → wird als `JwtAudience` in c-entron gespeichert |
| Authority URL | `https://login.microsoftonline.com/{tenant-id}/v2.0` → `JwtAuthority` |
| Redirect URI | MSAL Default für Public Client Apps |
| Token-Typ | ID-Token (nicht Access-Token) |
### c-entron Konfiguration
| Was | Wo | Setting ID |
|---|---|---|
| `JwtAuthority` | ApplicationSettings | 10351 |
| `JwtAudience` | ApplicationSettings | 10352 |
| `SystemAuthenticationMethod` | ApplicationSettings | 10360 (0=Any, 3=OpenIdConnect) |
| OpenIDConnect-Lizenz | Lizenztabelle | `AB4181F6-EF3B-4763-B29B-F5D0603311F7` |
### User-Verknüpfung
Jeder c-entron User braucht seine **Microsoft Entra Object ID** in der Spalte `OpenIdConnectSubjectIdentifier` (Tabelle `Sichbenu`). Verknüpfung über:
- **Self-Service:** `POST /jwt/connect_accounts`
- **Admin-Zuweisung:** WPF-UI unter "Persönliche Einstellungen"
---
## API-Endpoints im Überblick
| Endpoint | Methode | Auth | Zweck |
|---|---|---|---|
| `/config/jwt` | GET | Keine | JWT-Konfiguration abrufen |
| `/config/jwt` | PATCH | c-entron Ticket | JWT-Konfiguration ändern |
| `/jwt/login` | POST | Bearer (ID-Token) | **ID-Token → c-entron Ticket** |
| `/jwt/connect_accounts` | POST | Bearer (ID-Token) | Microsoft-Konto mit c-entron verknüpfen |
---
## Implementierungsbeispiel: OAuth-Token gegen c-entron Ticket tauschen
Minimales Beispiel für eine externe Applikation, die bereits ein Microsoft ID-Token hat und dieses gegen ein c-entron Ticket tauschen möchte.
### C# (.NET)
```csharp
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Identity.Client;
public class CentronOAuthClient
{
private readonly HttpClient _httpClient;
private readonly string _centronBaseUrl;
public CentronOAuthClient(string centronBaseUrl)
{
_centronBaseUrl = centronBaseUrl.TrimEnd('/');
_httpClient = new HttpClient { BaseAddress = new Uri(_centronBaseUrl) };
}
// ──────────────────────────────────────────────────────
// Schritt 1: JWT-Konfiguration vom c-entron Server holen
// ──────────────────────────────────────────────────────
public async Task<JwtConfiguration> GetJwtConfigurationAsync()
{
var response = await _httpClient.GetAsync("/config/jwt");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<JwtConfiguration>();
}
// ──────────────────────────────────────────────────────
// Schritt 2: Microsoft ID-Token über MSAL holen
// ──────────────────────────────────────────────────────
public async Task<string> AcquireMicrosoftIdTokenAsync(JwtConfiguration config)
{
var app = PublicClientApplicationBuilder
.Create(config.Audience) // Client ID aus c-entron Config
.WithAuthority(config.Authority) // Authority aus c-entron Config
.WithDefaultRedirectUri()
.Build();
string[] scopes = ["openid", "profile"];
AuthenticationResult result;
var accounts = await app.GetAccountsAsync();
var account = accounts.FirstOrDefault();
try
{
// Silent: aus Cache oder SSO
result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Interaktiv: Microsoft Login-Dialog zeigen
result = await app.AcquireTokenInteractive(scopes)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
return result.IdToken; // WICHTIG: IdToken, nicht AccessToken!
}
// ──────────────────────────────────────────────────────
// Schritt 3: ID-Token gegen c-entron Ticket tauschen
// ──────────────────────────────────────────────────────
public async Task<string> ExchangeTokenForTicketAsync(
string microsoftIdToken,
string applicationGuid,
string appVersion)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/jwt/login")
{
Content = JsonContent.Create(new
{
Application = applicationGuid,
AppVersion = appVersion,
Device = Environment.MachineName
})
};
request.Headers.Add("Authorization", $"Bearer {microsoftIdToken}");
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new Exception($"Login fehlgeschlagen: {response.StatusCode} — {error}");
}
// Response ist der Ticket-String (plain text)
return await response.Content.ReadAsStringAsync();
}
// ──────────────────────────────────────────────────────
// Kompletter Flow: Alles zusammen
// ──────────────────────────────────────────────────────
public async Task<string> LoginWithMicrosoftAsync(
string applicationGuid,
string appVersion)
{
// 1. JWT-Konfiguration abrufen
var config = await GetJwtConfigurationAsync();
if (!config.Enabled)
throw new Exception("OpenID Connect ist auf diesem Server nicht aktiviert.");
// 2. Microsoft ID-Token holen
var idToken = await AcquireMicrosoftIdTokenAsync(config);
// 3. Token gegen c-entron Ticket tauschen
var ticket = await ExchangeTokenForTicketAsync(idToken, applicationGuid, appVersion);
return ticket;
}
}
// ──────────────────────────────────────────────────────
// DTOs
// ──────────────────────────────────────────────────────
public class JwtConfiguration
{
public string Authority { get; set; }
public string Audience { get; set; }
public bool Enabled { get; set; }
}
```
### Verwendung
```csharp
var client = new CentronOAuthClient("https://mein-centron-server.example.com");
// Kompletter Flow
var ticket = await client.LoginWithMicrosoftAsync(
applicationGuid: "{verschlüsselte-lizenz-guid}",
appVersion: "1.0.0.0"
);
Console.WriteLine($"c-entron Ticket: {ticket}");
// → Ticket für alle weiteren API-Calls verwenden
```
### Minimales Beispiel: Nur Token-Tausch (wenn ID-Token bereits vorhanden)
```csharp
// Wenn du bereits ein Microsoft ID-Token hast (z.B. aus einer anderen Auth-Bibliothek):
var client = new CentronOAuthClient("https://mein-centron-server.example.com");
var ticket = await client.ExchangeTokenForTicketAsync(
microsoftIdToken: "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
applicationGuid: "{verschlüsselte-lizenz-guid}",
appVersion: "1.0.0.0"
);
```
### cURL-Beispiel
```bash
# 1. JWT-Konfiguration abrufen
curl -s https://mein-centron-server.example.com/config/jwt
# 2. ID-Token gegen Ticket tauschen (ID-Token aus MSAL o.ä.)
curl -X POST https://mein-centron-server.example.com/jwt/login \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{
"Application": "{verschlüsselte-lizenz-guid}",
"AppVersion": "1.0.0.0",
"Device": "MEIN-PC"
}'
# Response: ticket-hash-string (plain text)
```
### NuGet-Pakete
```xml
<PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
<!-- Optional, für Token-Cache-Persistierung: -->
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.*" />
<!-- Optional, für Windows WAM Broker (SSO): -->
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.*" />
```
---
## Relevante Quellcode-Dateien
| Schicht | Datei | Rolle |
|---|---|---|
| WPF View | `src/centron/Centron.WPF.UI/Modules/Administration/Connections/LoginDialogView.xaml` | Button "Anmelden mit Microsoft" |
| WPF ViewModel | `src/centron/Centron.WPF.UI/Modules/Administration/Connections/LoginDialogViewModel.cs` | Command-Handler |
| Client MSAL | `src/centron/Centron.WPF.UI/Services/WebServices/CentronWebServiceConnection.cs` | MSAL-Konfiguration, Token-Akquise, Token-Tausch |
| HTTP Client | `src/webservice/Centron.WebServices.Core/HttpClients/JwtAuthClient.cs` | `POST /jwt/login` mit Bearer-Header |
| Server Middleware | `src/webservice/Centron.Host/CentronHost.cs` | JWT Bearer Validierung |
| Server Controller | `src/webservice/Centron.Controllers/Controllers/Unversioned/JwtAuthController.cs` | `/jwt/login` Endpoint |
| Auth Factory | `src/backend/Centron.BL/Administration/Logins/Auth/AuthenticatorFactory.cs` | Routing zum OIDC-Authenticator |
| OIDC Authenticator | `src/backend/Centron.BL/Administration/Logins/Auth/OpenIdConnectAuthenticator.cs` | User-Lookup per `oid`-Claim |
| Ticket-Erstellung | `src/backend/Centron.BL/Administration/Logins/Auth/Authenticator.cs` + `TicketBL.cs` | Ticket generieren & speichern |
| Account Linking | `src/backend/Centron.BL/Administration/Logins/Auth/OpenIdConnectAccountConnector.cs` | Microsoft ↔ c-entron verknüpfen |
| JWT Config Model | `src/webservice/Centron.WebServices.Core/RestRequests/JwtConfiguration.cs` | Authority + Audience DTO |
| Login Request Model | `src/webservice/Centron.WebServices.Core/RestRequests/JwtLoginRequest.cs` | Application + AppVersion + Device |
| Settings IDs | `src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingID.cs` | JwtAuthority=10351, JwtAudience=10352 |
| Lizenz-GUIDs | `src/backend/Centron.Interfaces/Administration/Logins/LicenseGuids.cs` | OpenIDConnectAuthentication |
@@ -0,0 +1,25 @@
# Developer security
We have some security in place, to protect developers from accidentally doing something bad, for example sending mails to real customers.
These safeguards can't protect you from all accidents, so you still have to be careful when you're sending mails or calling external APIs.
But in most cases, you should be protected and safe.
All of these safeguards are configured in the `DeveloperSecurity.cs` file.
Check out the code in that file to get a detailed understanding of how it works and what it does.
## Sending emails
To protect us from accidentally sending emails to actual customer email addresses, there are some safeguards in place.
> :exclamation: These safeguards are only active in DEBUG-builds of the c-entron.NET :exclamation:
If you manually create a RELEASE-build of the c-entron.NET and send emails with it, it's up to you to as the developer to make sure that you don't send any emails to actual customer email addresses.
In DEBUG-builds all **external email addresses** will get replaced by `test@nexoware.com`.
The **internal email addresses** will not get modified at all.
> A email address is considered **internal** when it ends with `nexoware.com`.
> Every other email address is considered **external**.
If you want to disable this behavior (for example when you're trying to test your email sending code), you can manually edit the `AllowSendingEmailToExternalAddresses` property in the `DeveloperSecurity.cs` file.
@@ -0,0 +1,102 @@
# How does our licensing work?
## What is a license?
Our licenses are just simple GUIDs.
There is a GUID for `c-entron.NET`, another one for `Service-Board`, and again a different one for `Outlook Add-In`, etc.
But also **single features** can have their own GUID.
For example the `branch functionality`, or the `report server`, etc.
Those are all licenses a customer can potentially **have** or **NOT have**.
Additionally, each license can have a `count`, a `valid until date` and a `valid until version`, with either a `real value` or `unlimited`.
The license also has a name for display purposes only, technically the name is not relevant at all.
So, to summarize it again, for each license we have the following possible values:
* Does he have the license (`GUID`)?
* How many of them (`count`)?
* Until when is this license valid (`valid until date`)?
* Until which version is the license valid (`valid until version`)?
## How does the c-entron.NET and c-entron Web-Service work with those?
The c-entron.NET and c-entron Web-Service (also Riverbird Web-Service) generally differentiate between `Applications` and `Only Licenses`.
`Only Licenses` are the **single features** like the `branch functionality` or the `report server`.
They are all listed in the `LicenseGuids.cs` file.
Actually, every single license that we have is listed in the `LicenseGuids.cs` file, no matter if it's just a single license that we check for, or a `Application`.
`Applications` on the other hand are all licenses that are allowed to `Login` at the web-service.
They are all listed in the `ApplicationKind.cs` file.
Every entry in that file is allowed to `Login` at the web-service.
For all of those the `count`, `valid until date` and `valid until version` values are automatically checked and validated.
## Which licenses do we have?
The single source of truth for all our available licenses is the license-server.
You can use the `c-entron Office` tool to look at all the licenses, but usually that is not required.
We try to keep the `LicenseGuids.cs` file in sync with the license-server, to make it easier to check for licenses.
## I need a new license, what do I do?
At first, make sure we really have a `NEW THING` that needs to be separately licensed?
When you're sure, go to your development leader of your choice, and ask him to create this new license for you.
He will give you the `GUID` that represents this license.
Remember: Our licenses are just simple GUIDs.
You should add this new GUID to the `LicenseGuids.cs` file. And if it's required to `Login` at the web-service with it (in case for a new product), also add it to the `ApplicationKind.cs` file.
## Great, I got the GUID, how do I check for the license now?
If your license is a `Application` like we talked about above, then you might not need to do anything.
Just adding it the the `ApplicationKind.cs` is enough to allow you to login at the web-service, and have the `count`, `valid until date` and `valid until version` validated for you.
If you only have a simple boring license that you want to check, to show or hide a module in the c-entron.NET (like the `password manager` for example), or show some UI to the user, or enable extra functionality in any other way, you can use the `LicenseManager` to do that.
Let me just show you some code examples.
### Check if the customer has a license
Again, you can use this to hide or show UI, a module, or enable some features for a customer only.
```csharp
bool hasPasswordManager = LicenseManager.Instance.HasLicense(LicenseGuids.PasswordManager); // This is the important line
if (hasPasswordManager)
this.ShowPasswordManagerUI();
```
### Check the `count` of the license
This can for example be used, when we license something on a HOW MANY base.
Right now we do it for example for the `MyDay Import`.
This module can be used to import from external tools into c-entron for the `MyDay` module.
And we sell every import separately.
That means, a customer could buy 3 imports, and then would be allowed to configure 3 different imports.
On a more crazy, made up example, we could use this functionality to license how many articles the customer is allowed to create in the c-entron.
```csharp
Result<int?> myDayImportCountResult = LicenseManager.Instance.GetLicenseCount(LicenseGuids.MyDayImports); // This is the important line
if (myDayImportCountResult.Status == ResultStatus.Error)
{
// The customer does NOT have a license for LicenseGuids.MyDayImports
// Consider checking with LicenseManager.Instance.HasLicense first if the customer even has the license
}
else if (myDayImportCountResult.Status == ResultStatus.Success)
{
// The customer does have a license for LicenseGuids.MyDayImports, that's great!
// Lets now check how MANY of them he does have
// Again, this checks the COUNT of the license
int? licenseCount = myDayImportCountResult.Data;
if (licenseCount == null)
{
// The COUNT is UNLIMITED
}
else
{
// The COUNT is the number that is in licenseCount right now
// If the customer is allowed to use 3 MyDayImports, then licenseCount would be 3 here
}
}
```