Geräte-Authentifizierung (pp_*-Bearer)
Seit Welle 1C (Commit fb67297b6af, Build #22, 13.06.2026) verwenden alle
Endgeräte einen einheitlichen Bearer-Token zur Authentifizierung gegenüber
dem Standort-Server.
Token-Format
pp_<43-Zeichen-base64url>
- Präfix
pp_— erlaubt schnelle Sichterkennung und ein effizientes Header-Filtering (startsWith("pp_")). - 32 Byte Entropie aus
RandomNumberGenerator.GetBytes(32)→ base64url-codiert (kein Padding) = 43 Zeichen. - Gesamtlänge: 46 Zeichen.
Beispiel:
pp_xR4dKw9V5pNvB1tT2uHjE6_iZQOaJyMlKqWxYz0rA8c
Speicherung
Der Klartext-Token wird nur einmal beim Provisioning zurückgegeben und
nie in der DB gespeichert. Persistiert wird ausschliesslich der SHA256-
Hash (hex, lowercase, 64 Zeichen) in der Tabelle api_tokens:
CREATE TABLE api_tokens (
token_uuid CHAR(36) PRIMARY KEY,
token_hash CHAR(64) NOT NULL UNIQUE, -- SHA256 hex lowercase
geraet_uuid CHAR(36) NOT NULL,
mandant_uuid CHAR(36) NOT NULL,
standort_uuid CHAR(36) NOT NULL,
geraete_typ VARCHAR(40) NOT NULL,
bezeichnung VARCHAR(120) NULL,
erstellt_am DATETIME NOT NULL,
gueltig_bis DATETIME NULL,
letzte_nutzung_am DATETIME NULL,
widerrufen_am DATETIME NULL,
KEY idx_geraet (geraet_uuid)
);
Bei Benutzer-Passwörtern ist BCrypt (workFactor=10) Pflicht — wegen
schwacher Passwort-Entropie + Re-Use über Dienste hinweg.
Geräte-Tokens haben 256 Bit echte Entropie und werden nirgendwo
wiederverwendet. Brute-Force des Hash ist genauso schwer wie Brute-Force des
RNG selbst — SHA256 ist also ausreichend, schneller, und vermeidet das
BCrypt-Workfactor-Tuning auf schwacher Hardware.
Verwendung im Request
GET /api/v1/auth/device/me HTTP/1.1
Host: 10.1.1.20:8080
Authorization: Bearer pp_xR4dKw9V5pNvB1tT2uHjE6_iZQOaJyMlKqWxYz0rA8c
Der Header-Parser ist case-insensitive (bearer, Bearer, BEARER
funktionieren alle).
Server-seitiger Ablauf
Die Komponenten:
| Klasse | Datei | Verantwortung |
|---|---|---|
AuthTokenService | Api/Auth/AuthTokenService.cs | GeneriereKlartext(), Hash(klar), LeseBearer(req) |
DeviceAuthEndpointFilter | Api/Auth/DeviceAuthEndpointFilter.cs | IEndpointFilter, 401 bei Fehler, kontext-Set bei Erfolg |
ApiTokenRepository | Data/Repositories/ApiTokenRepository.cs | FindKontextByHashAsync, InsertAsync, UpdateLetzteNutzungAsync, WiderrufeAlleFuerGeraetAsync, ListAktiveAsync |
HttpContextExtensions | Api/Auth/HttpContextExtensions.cs | Get/SetzeGeraetKontext via ctx.Items["ProfiposGeraetKontext"] |
Im Program.cs wird der Filter scoped registriert und als
AddEndpointFilter<DeviceAuthEndpointFilter>() auf die Secure-Gruppe gelegt:
builder.Services
.AddScoped<IApiTokenRepository, ApiTokenRepository>()
.AddScoped<Profipos.Server.Api.Auth.DeviceAuthEndpointFilter>();
var v1Secure = app.MapGroup("/api/v1").WithTags("AppApiSecure")
.AddEndpointFilter<Profipos.Server.Api.Auth.DeviceAuthEndpointFilter>();
v1Secure.MapDeviceAuthApi();
v1Secure.MapTischeApiV2();
Token-Lifecycle
- Erzeugung — Backoffice oder Provisioning-Wizard erzeugt einen Token:
var klar = AuthTokenService.GeneriereKlartext(); // pp_xR4d…
var hash = AuthTokenService.Hash(klar); // 64-hex
await repo.InsertAsync(new ApiToken(…hash…));
// Klartext wird EINMAL an das Gerät übergeben, dann verworfen. - Verwendung — Gerät schickt
Authorization: Bearer <klar>bei jedem Call. Server hasht → DB-Lookup → Kontext setzen. letzte_nutzung_amwird fire-and-forget bei jedem erfolgreichen Call aktualisiert (kein await, blockiert den Request nicht).- Widerruf —
WiderrufeAlleFuerGeraetAsync(geraet_uuid)setztwiderrufen_am = NOW()für alle Tokens eines Geräts (z. B. bei Geräte-Verlust).
Fehler-Antworten
| HTTP | grund | Ursache |
|---|---|---|
| 401 | kein_bearer_token | Header fehlt komplett |
| 401 | ungueltiges_token_format | Header da, aber kein pp_…-Bearer |
| 401 | unbekanntes_oder_abgelaufenes_token | Hash nicht in DB ODER widerrufen ODER abgelaufen |
| 500 | interner_fehler | DB-Connection down etc. |
Welche Geräte nutzen welches Schema?
| Gerät | Token-Quelle | Header? |
|---|---|---|
| Kasse (WPF) | PIN-Login → POST /api/v1/auth/login (Cookie-Session) | nein, klassisch |
| Kasse (WPF) | optional DeviceLoginAsync mit Geräte-Token | ja |
| goapp (Kotlin) | OkHttp-Interceptor runBlocking { prefs.currentToken() } | ja (immer) |
| kundendisplay | Pairing-Coordinator schreibt Token nach Pair-Event in Settings | ja |
| kitchendisplay | Polling-Repository sendet Token-Header | ja |
| werbedisplay | Settings-UI → Token + Geräte-UUID manuell hinterlegt | ja |
| selfservice | Settings-UI | ja |
| lieferapp | Settings-UI | ja |
| LED-Modul (ESP32) | kein REST — MQTT-Username/Password im NVS | n/a |
| Drucker / ZVT | kein REST — passive Hardware | n/a |