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 897a224..ebca103 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 @@ -332,65 +332,101 @@ export class EntraIdService implements OnModuleInit { /** * Authorization-URL fuer MS365 Integration generieren. - * Verwendet erweiterte Scopes (Mail, Calendar, Tasks). + * Direkte URL-Konstruktion (kein MSAL) — umgeht MSAL v5 Kompatibilitaetsprobleme + * mit reinen Graph-API-Scopes (ohne openid). * @param state CSRF-Token * @param redirectUri Optionaler Override (fuer dynamischen Host-basierten URI) */ async getIntegrationAuthUrl(state: string, redirectUri?: string): Promise { - if (!this.msalClient) { + const ssoConfig = await this.loadConfigFromRedis(); + const tenantId = + ssoConfig?.tenantId ?? this.config.get('AZURE_TENANT_ID') ?? ''; + const clientId = + ssoConfig?.clientId ?? this.config.get('AZURE_CLIENT_ID') ?? ''; + + if (!tenantId || !clientId) { throw new ServiceUnavailableException( 'Microsoft SSO ist nicht konfiguriert', ); } - const authUrlRequest: AuthorizationUrlRequest = { - scopes: this.integrationScopes, - redirectUri: redirectUri || this.integrationRedirectUri, + const params = new URLSearchParams({ + client_id: clientId, + response_type: 'code', + redirect_uri: redirectUri || this.integrationRedirectUri, + scope: this.integrationScopes.join(' '), state, - prompt: 'consent', // Immer Zustimmung anfordern fuer neue Scopes - }; + prompt: 'consent', + response_mode: 'query', + }); - return this.msalClient.getAuthCodeUrl(authUrlRequest); + const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`; + this.logger.log(`M365 Integration Auth URL generiert (tenant: ${tenantId})`); + return authUrl; } /** - * Authorization Code gegen M365-Tokens tauschen. - * Gibt Access-Token, Refresh-Token und Ablaufzeit zurueck. + * Authorization Code gegen M365-Tokens tauschen (Standard OAuth2 POST). + * Direkte fetch-Implementierung analog zu refreshIntegrationToken — + * umgeht MSAL v5 Probleme mit acquireTokenByCode fuer Graph-Scopes. * @param code Authorization Code von Microsoft * @param redirectUri Muss exakt mit dem URI aus dem Auth-Request uebereinstimmen */ async handleIntegrationCallback(code: string, redirectUri?: string): Promise { - if (!this.msalClient) { + const ssoConfig = await this.loadConfigFromRedis(); + const tenantId = + ssoConfig?.tenantId ?? this.config.get('AZURE_TENANT_ID') ?? ''; + const clientId = + ssoConfig?.clientId ?? this.config.get('AZURE_CLIENT_ID') ?? ''; + const clientSecret = + ssoConfig?.clientSecret ?? + this.config.get('AZURE_CLIENT_SECRET') ?? + ''; + + if (!tenantId || !clientId || !clientSecret) { throw new ServiceUnavailableException( - 'Microsoft SSO ist nicht konfiguriert', + 'Azure-Konfiguration fehlt fuer Token-Exchange', ); } - const tokenRequest: AuthorizationCodeRequest = { + const params = new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, code, - scopes: this.integrationScopes, - redirectUri: redirectUri || this.integrationRedirectUri, - }; + redirect_uri: redirectUri || this.integrationRedirectUri, + grant_type: 'authorization_code', + scope: this.integrationScopes.join(' '), + }); - const response: AuthenticationResult = - await this.msalClient.acquireTokenByCode(tokenRequest); + const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + const resp = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }); + + if (!resp.ok) { + const err = (await resp.json()) as { + error_description?: string; + error?: string; + }; + throw new Error( + `Token-Exchange fehlgeschlagen: ${err.error_description ?? err.error ?? resp.statusText}`, + ); + } + + const data = (await resp.json()) as { + access_token: string; + refresh_token?: string; + expires_in: number; + }; this.logger.log('M365 Integration Token erhalten'); - // Refresh-Token aus MSAL-Antwort (erfordert offline_access Scope) - // In msal-node >= 2.x ist refreshToken in der Antwort verfuegbar - const rawResponse = response as AuthenticationResult & { - refreshToken?: string; - }; - - const refreshToken = rawResponse.refreshToken ?? ''; - const tenantId = - (response.account?.tenantId) ?? ''; - return { - accessToken: response.accessToken, - refreshToken, - expiresAt: response.expiresOn ?? new Date(Date.now() + 3600 * 1000), + accessToken: data.access_token, + refreshToken: data.refresh_token ?? '', + expiresAt: new Date(Date.now() + data.expires_in * 1000), scopes: this.integrationScopes, msTenantId: tenantId, };