OIDC — Login with EMPEDIAcloud

Ermöglichen Sie Benutzern, sich mit ihrem AuditCollector-Konto in Ihrer Anwendung anzumelden. EMPEDIAcloud ist ein vollständiger OpenID Connect Authorization Server — Authorization Code Flow, PKCE, RS256 ID-Tokens.

Discovery Document: Alle Endpoint-URLs automatisch abrufbar unter /.well-known/openid-configuration

1. OIDC-Client registrieren

Bevor Ihre App den Login-Flow starten kann, muss sie als Client registriert sein.

1
Dashboard öffnen

Navigieren Sie zu Einstellungen → Integrationen → OIDC-Apps oder rufen Sie direkt settings.html#integrations auf.

2
App-Typ wählen

Confidential — Server-seitige Apps (Node.js, PHP, Django). Erhalten ein client_secret.
Public — SPAs und Mobile Apps. Kein Secret; PKCE ist dann Pflicht.

3
Redirect URIs eintragen

Nur exakt hinterlegte URIs werden nach der Authentifizierung akzeptiert. http://localhost:3000/callback ist für die Entwicklung erlaubt.

4
Client Secret sichern

Das Secret wird einmalig angezeigt. Speichern Sie es sofort in einem Secret-Manager (z.B. Vault, GitHub Secrets). Es kann jederzeit rotiert werden.

Alternativ per API (nur mit Dashboard-Sitzung, also dem Login-Token der Weboberfläche — nicht mit einem Access-Token):

curl -X POST __APP_DOMAIN__/api/user/oidc-clients \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My App",
    "redirect_uris": ["https://myapp.example.com/callback"],
    "is_public": false,
    "allowed_scopes": ["openid", "email", "scan:read"]
  }'

Antwort enthält client_id und das einmalige client_secret.

1b. Dynamic Client Registration (für MCP-Clients wie Claude/ChatGPT)

Für Apps, die sich selbst registrieren sollen (z.B. AI-Agenten, die den MCP-Endpunkt /mcp nutzen), gibt es zusätzlich einen öffentlichen, unauthentifizierten Registrierungs-Endpunkt nach RFC 7591 — genau das, was der registration_endpoint im Discovery-Dokument angibt.

curl -X POST __APP_DOMAIN__/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Meine App",
    "redirect_uris": ["https://meineapp.example.com/callback"],
    "scope": "openid scan:read scan:write report:read",
    "token_endpoint_auth_method": "none"
  }'
Dynamisch selbstregistrierte Clients sind bewusst eingeschränkter als über den Dashboard-Weg erstellte:
  • require_consent wird immer erzwungen — der Nutzer sieht beim ersten Zugriff immer den Consent-Screen, egal was angefragt wird.
  • scope wird auf eine feste Allowlist zugeschnitten: openid, profile, email, offline_access, scan:read, scan:write, report:read, monitor:read (Monitoring nur lesend). code:audit und monitor:write sind ausgeschlossen — Quellcode-Zugriff und Änderungen am Monitoring bleiben dem Dashboard-Weg bzw. eigenen API-Zugängen vorbehalten.
  • redirect_uris müssen https:// oder eine Loopback-Adresse sein (http://localhost/http://127.0.0.1, wie von Claude Code und ähnlichen lokalen Clients genutzt).
  • Selbstregistrierung selbst gewährt keinen Datenzugriff — dafür ist weiterhin der volle Login + Consent-Screen unter /oauth/authorize nötig.

Superadmins sehen alle registrierten Clients (auch dynamisch registrierte, mit Badge gekennzeichnet) org-übergreifend unter Admin → OIDC Apps und können sie jederzeit deaktivieren.

2. Authorization Code Flow

Empfohlener Flow für alle App-Typen. Public Clients nutzen zusätzlich PKCE.

1
Authorization Request — Nutzer zum Login weiterleiten

Ihre App baut die Authorization URL und leitet den Browser des Nutzers dorthin um.

2
Login & Consent — AuditCollector zeigt Login-Formular

Der Nutzer meldet sich mit E-Mail/Passwort (oder Passkey) an und genehmigt die angeforderten Scopes.

3
Callback — Authorization Code an Ihre App

Nach Erfolg: Redirect zu Ihrer redirect_uri mit einem kurzlebigen code Parameter.

4
Token Exchange — Code gegen Tokens tauschen

Server-seitig: POST /oauth/token mit code + client_secret (oder PKCE code_verifier) → erhält access_token, id_token, refresh_token.

5
ID Token verifizieren — Nutzeridentität bestätigen

Das id_token ist ein RS256-signiertes JWT. Verifizieren mit dem öffentlichen Schlüssel von /.well-known/jwks.json.

Schritt 1 — Authorization URL aufbauen

import crypto from 'crypto';

// PKCE vorbereiten (für Public Clients und optional für Confidential)
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// State gegen CSRF
const state = crypto.randomBytes(16).toString('hex');

const params = new URLSearchParams({
  response_type: 'code',
  client_id: 'oidc_abc123',
  redirect_uri: 'https://myapp.example.com/callback',
  scope: 'openid email profile',
  state,
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
});

// Session speichern (vor dem Redirect)
session.oidcState = state;
session.codeVerifier = codeVerifier;

// Nutzer weiterleiten
res.redirect(`__APP_DOMAIN__/oauth/authorize?${params}`);

Schritt 2 — Callback verarbeiten

// GET /callback?code=abc&state=xyz
app.get('/callback', async (req, res) => {
  const { code, state, error } = req.query;

  if (error) return res.status(400).send(`OAuth Fehler: ${error}`);

  // State prüfen (CSRF-Schutz)
  if (state !== req.session.oidcState) {
    return res.status(400).send('Ungültiger State');
  }

  // Token Exchange
  const tokenRes = await fetch('__APP_DOMAIN__/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://myapp.example.com/callback',
      client_id: 'oidc_abc123',
      client_secret: process.env.OIDC_CLIENT_SECRET,  // Confidential Client
      code_verifier: req.session.codeVerifier,          // PKCE
    }),
  });

  const tokens = await tokenRes.json();
  // tokens.access_token, tokens.id_token, tokens.refresh_token
  // → ID Token verifizieren (Schritt 3)
});

Schritt 3 — ID Token verifizieren

Das id_token ist ein RS256-signiertes JWT. Verwenden Sie eine bewährte Bibliothek:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('__APP_DOMAIN__/.well-known/jwks.json')
);

async function verifyIdToken(idToken) {
  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer: '__APP_DOMAIN__',
    audience: 'oidc_abc123',
  });
  return payload;
  // payload.sub   → Nutzer-ID (stabil, unveränderlich)
  // payload.email → E-Mail-Adresse
  // payload.name  → Anzeigename
}
Niemals das ID Token client-seitig ohne Signaturverifikation auslesen. Nutzen Sie jose (Node.js), python-jose (Python) oder firebase/php-jwt (PHP) — diese Bibliotheken holen und cachen die Public Keys automatisch.

Beispiel: PHP (Confidential Client)

<?php
// Schritt 1: Weiterleiten
$state = bin2hex(random_bytes(16));
$_SESSION['oidc_state'] = $state;

$params = http_build_query([
    'response_type' => 'code',
    'client_id'     => 'oidc_abc123',
    'redirect_uri'  => 'https://myapp.example.com/callback',
    'scope'         => 'openid email profile',
    'state'         => $state,
]);
header('Location: __APP_DOMAIN__/oauth/authorize?' . $params);
exit;

// Schritt 2: Callback (callback.php)
if ($_GET['state'] !== $_SESSION['oidc_state']) die('Ungültiger State');

$response = file_get_contents('__APP_DOMAIN__/oauth/token', false,
    stream_context_create(['http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'grant_type'    => 'authorization_code',
            'code'          => $_GET['code'],
            'redirect_uri'  => 'https://myapp.example.com/callback',
            'client_id'     => 'oidc_abc123',
            'client_secret' => getenv('OIDC_CLIENT_SECRET'),
        ]),
    ]])
);

$tokens = json_decode($response, true);
// ID Token mit firebase/php-jwt verifizieren
// $payload = JWT::decode($tokens['id_token'], JWK::parseKeySet($jwks), ['RS256']);

Verfügbare Scopes

ScopeClaims / Zugriff
openidPflicht — gibt sub (stabile Nutzer-ID) im ID Token zurück
emailemail, email_verified
profilename, given_name, family_name
offline_accessGibt ein refresh_token zurück (Token-Erneuerung ohne erneuten Login)
scan:readScan-Ergebnisse der Organisation lesen (über Access-Token)
scan:writeNeue Scans starten
report:readBerichte und Findings lesen
code:auditCode-Audit ausführen
monitor:read / monitor:writeMonitoring lesen bzw. verwalten (nur für eigene Service-Clients und manuell angelegte Apps; per Selbstregistrierung nicht erhältlich)

ID Token Claims

sub
Stabile Nutzer-ID (UUID) — unveränderlich, auch nach E-Mail-Änderung
iss
Issuer — immer __APP_DOMAIN__ (oder Ihre Instanz-URL)
aud
Audience — Ihre client_id; muss bei der Verifikation geprüft werden
email
E-Mail-Adresse des Nutzers (Scope email erforderlich)
email_verified
true wenn die E-Mail bestätigt wurde
name
Anzeigename (Scope profile erforderlich)
exp / iat
Ablaufzeit / Ausstellungszeit (Unix Timestamp) — ID Tokens sind 1 Stunde gültig

Access Token erneuern (Refresh)

Mit Scope offline_access erhalten Sie ein refresh_token. Damit können Sie neue Access Tokens ohne erneuten Login anfordern:

const res = await fetch('__APP_DOMAIN__/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: 'oidc_abc123',
    client_secret: process.env.OIDC_CLIENT_SECRET,
  }),
});
const { access_token, id_token } = await res.json();

Passport.js (Node.js)

import passport from 'passport';
import { Strategy as OidcStrategy } from 'openid-client';
import { Issuer } from 'openid-client';

const issuer = await Issuer.discover('__APP_DOMAIN__');

const client = new issuer.Client({
  client_id: 'oidc_abc123',
  client_secret: process.env.OIDC_CLIENT_SECRET,
  redirect_uris: ['https://myapp.example.com/callback'],
  response_types: ['code'],
});

passport.use('oidc', new OidcStrategy({ client }, async (tokenSet, userinfo, done) => {
  const user = await User.findOrCreate({ sub: userinfo.sub, email: userinfo.email });
  return done(null, user);
}));

// Routes
app.get('/login', passport.authenticate('oidc', { scope: 'openid email profile' }));
app.get('/callback', passport.authenticate('oidc', { failureRedirect: '/login' }),
  (req, res) => res.redirect('/dashboard'));

Sicherheitshinweise

  • State-Parameter — immer prüfen, verhindert CSRF-Angriffe
  • PKCE — für Public Clients Pflicht, für Confidential Clients empfohlen
  • Client Secret — nur server-seitig verwenden, nie im Frontend/SPA
  • ID Token Signatur — immer mit JWKS verifizieren, niemals blind vertrauen
  • Redirect URI — exakt hinterlegen, keine Wildcards
  • Token Storage — Access Tokens in Memory (nicht localStorage), Refresh Tokens in HttpOnly Cookies

Nächste Schritte