2026-06-03 11:15:59 +00:00
|
|
|
// DocuWare-Authentifizierung.
|
|
|
|
|
//
|
|
|
|
|
// Primär: DocuWare Identity Service (OpenID Connect).
|
|
|
|
|
// Neuere DocuWare-Versionen / DocuWare Cloud haben das alte Cookie-Logon
|
|
|
|
|
// (/Account/Logon) abgeschaltet (HTTP 410) und verlangen ein OAuth-Token.
|
|
|
|
|
// Wir nutzen den Resource-Owner-Password-Grant (ROPC) mit dem von DocuWare
|
|
|
|
|
// vorinstallierten ÖFFENTLICHEN Client "docuware.platform.net.client" --
|
|
|
|
|
// d.h. KEINE eigene App-Registrierung und KEIN Client-Secret nötig.
|
|
|
|
|
//
|
|
|
|
|
// Ablauf:
|
|
|
|
|
// 1. GET {platform}/Home/IdentityServiceInfo -> Identity-Service-URL
|
|
|
|
|
// 2. GET {identity}/.well-known/openid-configuration -> token_endpoint
|
|
|
|
|
// 3. POST {token_endpoint} grant_type=password ... -> access_token
|
|
|
|
|
// 4. Folgeaufrufe der Platform-API mit Authorization: Bearer <token>
|
|
|
|
|
//
|
|
|
|
|
// Fallback: Findet sich kein Identity Service (alte On-Prem-Server), wird das
|
|
|
|
|
// klassische Cookie-Logon versucht.
|
|
|
|
|
//
|
2026-06-03 13:14:05 +00:00
|
|
|
// Token wird NUR im Speicher gehalten (TOKEN), nie persistiert. Im Hintergrund-
|
|
|
|
|
// skript überlebt es die TB-Sitzung und wird per Messaging an die Fenster gereicht
|
|
|
|
|
// (siehe background.js). Das Passwort wird ausschließlich zum Holen des Tokens
|
|
|
|
|
// verwendet und danach verworfen.
|
2026-06-03 11:15:59 +00:00
|
|
|
|
|
|
|
|
const DW_PUBLIC_CLIENT_ID = "docuware.platform.net.client";
|
|
|
|
|
const DW_SCOPE = "docuware.platform";
|
2026-06-03 13:14:05 +00:00
|
|
|
const COOKIE_TTL_MS = 8 * 60 * 60 * 1000; // Cookie-Session pragmatisch ~8h gültig
|
2026-06-03 11:15:59 +00:00
|
|
|
|
2026-06-03 13:14:05 +00:00
|
|
|
let TOKEN = null; // { accessToken, expiresAt, mode, tokenEndpoint }
|
2026-06-03 11:15:59 +00:00
|
|
|
|
|
|
|
|
const Auth = {
|
|
|
|
|
/**
|
|
|
|
|
* Meldet sich an DocuWare an. Wirft bei Fehlschlag.
|
|
|
|
|
* @returns {Promise<{mode:string}>}
|
|
|
|
|
*/
|
|
|
|
|
async logon(settings) {
|
|
|
|
|
const identityUrl = await this._discoverIdentityService(settings);
|
|
|
|
|
if (identityUrl) {
|
|
|
|
|
await this._logonIdentity(settings, identityUrl);
|
|
|
|
|
return { mode: "identity" };
|
|
|
|
|
}
|
|
|
|
|
// Fallback: altes Cookie-Logon (nur ältere On-Prem-Server).
|
|
|
|
|
await this._logonCookie(settings);
|
|
|
|
|
return { mode: "cookie" };
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Zusätzliche Header für Requests. Bearer-Token, falls vorhanden. */
|
|
|
|
|
authHeaders() {
|
|
|
|
|
if (TOKEN && TOKEN.accessToken) {
|
|
|
|
|
return { Authorization: `Bearer ${TOKEN.accessToken}` };
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-03 13:14:05 +00:00
|
|
|
/** Aktuell ein Token vorhanden und (noch) gültig? (auch Cookie-Session) */
|
2026-06-03 11:15:59 +00:00
|
|
|
hasValidToken() {
|
2026-06-03 13:14:05 +00:00
|
|
|
return !!(
|
|
|
|
|
TOKEN &&
|
|
|
|
|
(TOKEN.accessToken || TOKEN.mode === "cookie") &&
|
|
|
|
|
TOKEN.expiresAt > Date.now() + 5000
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Serialisierbare Token-Sicht für den Transfer per Messaging. */
|
|
|
|
|
currentToken() {
|
|
|
|
|
if (!TOKEN) return null;
|
|
|
|
|
return { accessToken: TOKEN.accessToken || null, expiresAt: TOKEN.expiresAt, mode: TOKEN.mode };
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Übernimmt ein vom Broker (Hintergrundskript) geliefertes Token in diesen
|
|
|
|
|
* Kontext, damit authHeaders()/credentials greifen. Kein Passwort nötig.
|
|
|
|
|
*/
|
|
|
|
|
setToken(t) {
|
|
|
|
|
if (!t || (!t.accessToken && t.mode !== "cookie")) {
|
|
|
|
|
TOKEN = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
TOKEN = {
|
|
|
|
|
accessToken: t.accessToken || null,
|
|
|
|
|
expiresAt: t.expiresAt || Date.now() + COOKIE_TTL_MS,
|
|
|
|
|
mode: t.mode || (t.accessToken ? "identity" : "cookie"),
|
|
|
|
|
};
|
2026-06-03 11:15:59 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** Token verwerfen (z.B. beim Wechsel der Zugangsdaten). */
|
|
|
|
|
reset() {
|
|
|
|
|
TOKEN = null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// --- Identity Service -----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ermittelt die Identity-Service-URL. Liefert null, wenn der Server keinen
|
|
|
|
|
* Identity Service meldet (dann Cookie-Fallback).
|
|
|
|
|
*/
|
|
|
|
|
async _discoverIdentityService(settings) {
|
|
|
|
|
const platform = Settings.platformUrl(settings);
|
|
|
|
|
let res;
|
|
|
|
|
try {
|
|
|
|
|
res = await fetch(`${platform}/Home/IdentityServiceInfo`, {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: { Accept: "application/json" },
|
|
|
|
|
});
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) return null;
|
|
|
|
|
const info = await res.json().catch(() => ({}));
|
|
|
|
|
const url =
|
|
|
|
|
info.IdentityServiceUrl ||
|
|
|
|
|
info.identityServiceUrl ||
|
|
|
|
|
info.Url ||
|
|
|
|
|
info.url ||
|
|
|
|
|
null;
|
|
|
|
|
return url ? url.replace(/\/+$/, "") : null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async _logonIdentity(settings, identityUrl) {
|
|
|
|
|
// OpenID-Discovery -> token_endpoint.
|
|
|
|
|
const discoRes = await fetch(`${identityUrl}/.well-known/openid-configuration`, {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: { Accept: "application/json" },
|
|
|
|
|
});
|
|
|
|
|
if (!discoRes.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Identity Service nicht erreichbar (HTTP ${discoRes.status} bei ${identityUrl}).`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const disco = await discoRes.json();
|
|
|
|
|
const tokenEndpoint = disco.token_endpoint;
|
|
|
|
|
if (!tokenEndpoint) throw new Error("token_endpoint im Identity Service nicht gefunden.");
|
|
|
|
|
|
|
|
|
|
const body = new URLSearchParams({
|
|
|
|
|
grant_type: "password",
|
|
|
|
|
scope: DW_SCOPE,
|
|
|
|
|
client_id: DW_PUBLIC_CLIENT_ID,
|
|
|
|
|
username: settings.username,
|
|
|
|
|
password: settings.password,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const res = await fetch(tokenEndpoint, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
|
|
|
Accept: "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: body.toString(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
if (!res.ok || !data.access_token) {
|
|
|
|
|
const detail =
|
|
|
|
|
data.error_description || data.error || (await this._safeText(res));
|
|
|
|
|
throw new Error(`Identity-Logon fehlgeschlagen (HTTP ${res.status}). ${detail || ""}`.trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TOKEN = {
|
|
|
|
|
accessToken: data.access_token,
|
|
|
|
|
expiresAt: Date.now() + (Number(data.expires_in) || 3600) * 1000,
|
2026-06-03 13:14:05 +00:00
|
|
|
mode: "identity",
|
2026-06-03 11:15:59 +00:00
|
|
|
tokenEndpoint,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// --- Cookie-Fallback (alte Server) ----------------------------------------
|
|
|
|
|
|
|
|
|
|
async _logonCookie(settings) {
|
|
|
|
|
const platform = Settings.platformUrl(settings);
|
|
|
|
|
const body = new URLSearchParams({
|
|
|
|
|
UserName: settings.username,
|
|
|
|
|
Password: settings.password,
|
|
|
|
|
Organization: settings.organization,
|
|
|
|
|
RedirectToMyselfInCaseOfError: "false",
|
|
|
|
|
RememberMe: "false",
|
|
|
|
|
});
|
|
|
|
|
const res = await fetch(`${platform}/Account/Logon`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
credentials: "include",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
|
|
|
Accept: "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: body.toString(),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const text = await this._safeText(res);
|
|
|
|
|
throw new Error(`Logon fehlgeschlagen (HTTP ${res.status}). ${text}`.trim());
|
|
|
|
|
}
|
2026-06-03 13:14:05 +00:00
|
|
|
// Cookie-Session: kein Bearer-Token, Folgeaufrufe per credentials:"include".
|
|
|
|
|
// Wir merken nur, dass eine Session besteht (für hasValidToken/Sitzungs-Cache).
|
|
|
|
|
TOKEN = { accessToken: null, expiresAt: Date.now() + COOKIE_TTL_MS, mode: "cookie" };
|
2026-06-03 11:15:59 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async _safeText(res) {
|
|
|
|
|
return res.text().catch(() => "");
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (typeof module !== "undefined") module.exports = { Auth };
|