mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
fix(ms365): dynamische Redirect-URI aus Request-Host + Azure-Kompatibilität
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 <noreply@anthropic.com>
This commit is contained in:
parent
1ecd7dad82
commit
254d00c106
2 changed files with 51 additions and 16 deletions
|
|
@ -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<string>('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<string>('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<string> {
|
||||
async getIntegrationAuthUrl(state: string, redirectUri?: string): Promise<string> {
|
||||
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<M365TokenResult> {
|
||||
async handleIntegrationCallback(code: string, redirectUri?: string): Promise<M365TokenResult> {
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue