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.
/.well-known/openid-configuration
1. OIDC-Client registrieren
Bevor Ihre App den Login-Flow starten kann, muss sie als Client registriert sein.
Navigieren Sie zu Einstellungen → Integrationen → OIDC-Apps oder rufen Sie direkt settings.html#integrations auf.
Confidential — Server-seitige Apps (Node.js, PHP, Django). Erhalten ein client_secret.
Public — SPAs und Mobile Apps. Kein Secret; PKCE ist dann Pflicht.
Nur exakt hinterlegte URIs werden nach der Authentifizierung akzeptiert. http://localhost:3000/callback ist für die Entwicklung erlaubt.
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"
}'
require_consentwird immer erzwungen — der Nutzer sieht beim ersten Zugriff immer den Consent-Screen, egal was angefragt wird.scopewird auf eine feste Allowlist zugeschnitten:openid,profile,email,offline_access,scan:read,scan:write,report:read,monitor:read(Monitoring nur lesend).code:auditundmonitor:writesind ausgeschlossen — Quellcode-Zugriff und Änderungen am Monitoring bleiben dem Dashboard-Weg bzw. eigenen API-Zugängen vorbehalten.redirect_urismüssenhttps://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/authorizenö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.
Ihre App baut die Authorization URL und leitet den Browser des Nutzers dorthin um.
Der Nutzer meldet sich mit E-Mail/Passwort (oder Passkey) an und genehmigt die angeforderten Scopes.
Nach Erfolg: Redirect zu Ihrer redirect_uri mit einem kurzlebigen code Parameter.
Server-seitig: POST /oauth/token mit code + client_secret (oder PKCE code_verifier) → erhält access_token, id_token, refresh_token.
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
}
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
| Scope | Claims / Zugriff |
|---|---|
| openid | Pflicht — gibt sub (stabile Nutzer-ID) im ID Token zurück |
email, email_verified | |
| profile | name, given_name, family_name |
| offline_access | Gibt ein refresh_token zurück (Token-Erneuerung ohne erneuten Login) |
| scan:read | Scan-Ergebnisse der Organisation lesen (über Access-Token) |
| scan:write | Neue Scans starten |
| report:read | Berichte und Findings lesen |
| code:audit | Code-Audit ausführen |
| monitor:read / monitor:write | Monitoring lesen bzw. verwalten (nur für eigene Service-Clients und manuell angelegte Apps; per Selbstregistrierung nicht erhältlich) |
ID Token Claims
__APP_DOMAIN__ (oder Ihre Instanz-URL)client_id; muss bei der Verifikation geprüft werdenemail erforderlich)true wenn die E-Mail bestätigt wurdeprofile erforderlich)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
- API-Referenz: OIDC Provider — alle Endpunkte im Detail
- OIDC-Client per API registrieren
- Discovery Document — live ansehen
- Erste Schritte mit der API — Server-zu-Server mit Client Credentials
- EMPEDIAcloud mit Claude/ChatGPT verbinden — der MCP-Endpunkt nutzt genau diesen OAuth-Flow