refactor: reuse the SSO app registration for M365 calendar sync

One Entra enterprise application now covers both interactive login and
the calendar sync's client-credentials Graph access, instead of
requiring a second app registration. GraphClientService reads
OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET (same as SSO) and
derives the tenant ID from the issuer URL's path, rather than three
dedicated M365_CALENDAR_* env vars. The app registration just needs the
"Calendars.ReadWrite" Application permission added alongside its
existing delegated login scopes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-04 14:35:18 +02:00
1 parent e7b157ec35
commit e133a677ab
6 files changed
+70 -44

No files matched your search

@@ -27,15 +27,34 @@ function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
// Reuses the same Entra app registration as interactive SSO login
// (OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET) for the app-only
// Graph client-credentials flow, instead of a second, separate app
// registration - one enterprise application covers both, as long as it
// also has the "Calendars.ReadWrite" Application permission (admin-
// consented) alongside its delegated login scopes. OIDC_ISSUER_URL is
// "https://login.microsoftonline.com/<tenant-id>/v2.0" - the tenant id is
// its first path segment.
function tenantIdFromIssuerUrl(issuerUrl: string): string | null {
try {
const segments = new URL(issuerUrl).pathname.split('/').filter(Boolean);
return segments[0] ?? null;
} catch {
return null;
}
}
@Injectable()
export class GraphClientService {
private cachedToken: CachedToken | null = null;
isConfigured(): boolean {
const issuerUrl = process.env.OIDC_ISSUER_URL;
return Boolean(
process.env.M365_CALENDAR_TENANT_ID &&
process.env.M365_CALENDAR_CLIENT_ID &&
process.env.M365_CALENDAR_CLIENT_SECRET,
issuerUrl &&
tenantIdFromIssuerUrl(issuerUrl) &&
process.env.OIDC_CLIENT_ID &&
process.env.OIDC_CLIENT_SECRET,
);
}
@@ -44,12 +63,13 @@ export class GraphClientService {
return this.cachedToken.accessToken;
}
const tenantId = process.env.M365_CALENDAR_TENANT_ID;
const clientId = process.env.M365_CALENDAR_CLIENT_ID;
const clientSecret = process.env.M365_CALENDAR_CLIENT_SECRET;
const issuerUrl = process.env.OIDC_ISSUER_URL;
const clientId = process.env.OIDC_CLIENT_ID;
const clientSecret = process.env.OIDC_CLIENT_SECRET;
const tenantId = issuerUrl ? tenantIdFromIssuerUrl(issuerUrl) : null;
if (!tenantId || !clientId || !clientSecret) {
throw new Error(
'M365-Kalender-Synchronisation ist nicht konfiguriert (Env-Variablen fehlen)',
'M365-Kalender-Synchronisation ist nicht konfiguriert (SSO-Env-Variablen fehlen)',
);
}