Zum Hauptinhalt springen

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)
);
Warum SHA256 statt BCrypt?

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:

KlasseDateiVerantwortung
AuthTokenServiceApi/Auth/AuthTokenService.csGeneriereKlartext(), Hash(klar), LeseBearer(req)
DeviceAuthEndpointFilterApi/Auth/DeviceAuthEndpointFilter.csIEndpointFilter, 401 bei Fehler, kontext-Set bei Erfolg
ApiTokenRepositoryData/Repositories/ApiTokenRepository.csFindKontextByHashAsync, InsertAsync, UpdateLetzteNutzungAsync, WiderrufeAlleFuerGeraetAsync, ListAktiveAsync
HttpContextExtensionsApi/Auth/HttpContextExtensions.csGet/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

  1. 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.
  2. Verwendung — Gerät schickt Authorization: Bearer <klar> bei jedem Call. Server hasht → DB-Lookup → Kontext setzen.
  3. letzte_nutzung_am wird fire-and-forget bei jedem erfolgreichen Call aktualisiert (kein await, blockiert den Request nicht).
  4. WiderrufWiderrufeAlleFuerGeraetAsync(geraet_uuid) setzt widerrufen_am = NOW() für alle Tokens eines Geräts (z. B. bei Geräte-Verlust).

Fehler-Antworten

HTTPgrundUrsache
401kein_bearer_tokenHeader fehlt komplett
401ungueltiges_token_formatHeader da, aber kein pp_…-Bearer
401unbekanntes_oder_abgelaufenes_tokenHash nicht in DB ODER widerrufen ODER abgelaufen
500interner_fehlerDB-Connection down etc.

Welche Geräte nutzen welches Schema?

GerätToken-QuelleHeader?
Kasse (WPF)PIN-Login → POST /api/v1/auth/login (Cookie-Session)nein, klassisch
Kasse (WPF)optional DeviceLoginAsync mit Geräte-Tokenja
goapp (Kotlin)OkHttp-Interceptor runBlocking { prefs.currentToken() }ja (immer)
kundendisplayPairing-Coordinator schreibt Token nach Pair-Event in Settingsja
kitchendisplayPolling-Repository sendet Token-Headerja
werbedisplaySettings-UI → Token + Geräte-UUID manuell hinterlegtja
selfserviceSettings-UIja
lieferappSettings-UIja
LED-Modul (ESP32)kein REST — MQTT-Username/Password im NVSn/a
Drucker / ZVTkein REST — passive Hardwaren/a

Verwandte Themen