From 254d00c1063334dbd677b9703efe81baefb9ca68 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Thu, 12 Mar 2026 23:01:09 +0100 Subject: [PATCH] =?UTF-8?q?fix(ms365):=20dynamische=20Redirect-URI=20aus?= =?UTF-8?q?=20Request-Host=20+=20Azure-Kompatibilit=C3=A4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Redirect-URI wurde falsch aus SSO-URI abgeleitet, und unterstützte nur eine feste URL statt sowohl IP als auch DNS-Name. Lösung: - initM365Integration: Host aus x-forwarded-host/host Header lesen, korrekte Redirect-URI bauen (proto://host/api/v1/auth/integrations/...) - Redis-State speichert jetzt {userId, redirectUri} als JSON - handleIntegrationCallback: gespeicherte redirectUri aus State verwenden - getIntegrationAuthUrl/handleIntegrationCallback: optionaler redirectUri-Parameter - Fallback-Derivation: base URL aus SSO-URI + fester Integrations-Pfad Beide URIs müssen in Azure registriert sein: - http://172.20.10.59/api/v1/auth/integrations/microsoft-365/callback - http://insight.xinion.lan/api/v1/auth/integrations/microsoft-365/callback Co-Authored-By: Claude Sonnet 4.6 --- .../src/core/auth/sso/entra-id.service.ts | 29 ++++++++++---- .../integrations/integrations.controller.ts | 38 +++++++++++++++---- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/core-service/src/core/auth/sso/entra-id.service.ts b/packages/core-service/src/core/auth/sso/entra-id.service.ts index d733e6d..897a224 100644 --- a/packages/core-service/src/core/auth/sso/entra-id.service.ts +++ b/packages/core-service/src/core/auth/sso/entra-id.service.ts @@ -141,10 +141,19 @@ export class EntraIdService implements OnModuleInit { this.msalClient = new ConfidentialClientApplication(msalConfig); this.redirectUri = ssoConfig.redirectUri; - // Integration-Redirect-URI aus Env (Fallback: SSO-URI + '/integration') - this.integrationRedirectUri = - this.config.get('AZURE_INTEGRATION_REDIRECT_URI') || - ssoConfig.redirectUri.replace('/callback', '/integration/callback'); + // Integration-Redirect-URI: Env-Variable hat Vorrang. + // Fallback: Base-URL der SSO-URI + fester Pfad + const explicitIntegrationUri = this.config.get('AZURE_INTEGRATION_REDIRECT_URI'); + if (explicitIntegrationUri) { + this.integrationRedirectUri = explicitIntegrationUri; + } else { + try { + const ssoUrl = new URL(ssoConfig.redirectUri); + this.integrationRedirectUri = `${ssoUrl.protocol}//${ssoUrl.host}/api/v1/auth/integrations/microsoft-365/callback`; + } catch { + this.integrationRedirectUri = 'http://localhost:3000/api/v1/auth/integrations/microsoft-365/callback'; + } + } } /** @@ -324,8 +333,10 @@ export class EntraIdService implements OnModuleInit { /** * Authorization-URL fuer MS365 Integration generieren. * Verwendet erweiterte Scopes (Mail, Calendar, Tasks). + * @param state CSRF-Token + * @param redirectUri Optionaler Override (fuer dynamischen Host-basierten URI) */ - async getIntegrationAuthUrl(state: string): Promise { + async getIntegrationAuthUrl(state: string, redirectUri?: string): Promise { if (!this.msalClient) { throw new ServiceUnavailableException( 'Microsoft SSO ist nicht konfiguriert', @@ -334,7 +345,7 @@ export class EntraIdService implements OnModuleInit { const authUrlRequest: AuthorizationUrlRequest = { scopes: this.integrationScopes, - redirectUri: this.integrationRedirectUri, + redirectUri: redirectUri || this.integrationRedirectUri, state, prompt: 'consent', // Immer Zustimmung anfordern fuer neue Scopes }; @@ -345,8 +356,10 @@ export class EntraIdService implements OnModuleInit { /** * Authorization Code gegen M365-Tokens tauschen. * Gibt Access-Token, Refresh-Token und Ablaufzeit zurueck. + * @param code Authorization Code von Microsoft + * @param redirectUri Muss exakt mit dem URI aus dem Auth-Request uebereinstimmen */ - async handleIntegrationCallback(code: string): Promise { + async handleIntegrationCallback(code: string, redirectUri?: string): Promise { if (!this.msalClient) { throw new ServiceUnavailableException( 'Microsoft SSO ist nicht konfiguriert', @@ -356,7 +369,7 @@ export class EntraIdService implements OnModuleInit { const tokenRequest: AuthorizationCodeRequest = { code, scopes: this.integrationScopes, - redirectUri: this.integrationRedirectUri, + redirectUri: redirectUri || this.integrationRedirectUri, }; const response: AuthenticationResult = diff --git a/packages/core-service/src/core/integrations/integrations.controller.ts b/packages/core-service/src/core/integrations/integrations.controller.ts index 2e82b4b..c4c0067 100644 --- a/packages/core-service/src/core/integrations/integrations.controller.ts +++ b/packages/core-service/src/core/integrations/integrations.controller.ts @@ -3,10 +3,12 @@ import { Get, Delete, Query, + Req, Res, Logger, Param, } from '@nestjs/common'; +import { Request } from 'express'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; @@ -60,18 +62,27 @@ export class IntegrationsController { @ApiOperation({ summary: 'Microsoft 365 Integration starten' }) async initM365Integration( @CurrentUser() user: JwtUser, + @Req() req: Request, ): Promise<{ success: boolean; data: { url: string } }> { - // State = UUID, in Redis mit userId hinterlegen (5 Min TTL) + // Redirect-URI dynamisch aus dem Anfrage-Host ableiten + // Unterstuetzt sowohl IP als auch DNS-Name (z.B. insight.xinion.lan) + const host = (req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim(); + const proto = req.get('x-forwarded-proto') || req.protocol || 'http'; + const redirectUri = host + ? `${proto}://${host}/api/v1/auth/integrations/microsoft-365/callback` + : undefined; + + // State = UUID, in Redis hinterlegen: {userId, redirectUri} — 5 Min TTL const state = uuidv4(); await this.redis.set( `integration_state:${state}`, - user.sub, + JSON.stringify({ userId: user.sub, redirectUri: redirectUri ?? '' }), 300, ); - const authUrl = await this.entraIdService.getIntegrationAuthUrl(state); + const authUrl = await this.entraIdService.getIntegrationAuthUrl(state, redirectUri); this.logger.log( - `M365-Integration OAuth-Flow gestartet fuer User ${user.sub}`, + `M365-Integration OAuth-Flow gestartet fuer User ${user.sub} (redirectUri: ${redirectUri ?? 'default'})`, ); return { success: true, data: { url: authUrl } }; } @@ -108,17 +119,28 @@ export class IntegrationsController { return; } - // State aus Redis laden (enthaelt userId) - const userId = await this.redis.get(`integration_state:${state}`); - if (!userId) { + // State aus Redis laden (enthaelt {userId, redirectUri}) + const rawState = await this.redis.get(`integration_state:${state}`); + if (!rawState) { this.logger.warn('M365-Integration State ungueltig oder abgelaufen'); res.redirect(`${errorBase}&message=Sitzung+abgelaufen`); return; } await this.redis.del(`integration_state:${state}`); + let userId: string; + let savedRedirectUri: string | undefined; try { - const tokenResult = await this.entraIdService.handleIntegrationCallback(code); + const parsed = JSON.parse(rawState) as { userId: string; redirectUri?: string }; + userId = parsed.userId; + savedRedirectUri = parsed.redirectUri || undefined; + } catch { + // Fallback: alter Format (nur userId als String) + userId = rawState; + } + + try { + const tokenResult = await this.entraIdService.handleIntegrationCallback(code, savedRedirectUri); await this.integrationsService.saveM365Integration(userId, tokenResult); this.logger.log(`M365-Integration erfolgreich fuer User ${userId}`); res.redirect(successUrl);