mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
fix(ms365): direkte OAuth2 URL-Konstruktion statt MSAL für Integration-Flow
MSAL-node v5 erzeugt bei getAuthCodeUrl mit reinen Graph-API-Scopes (ohne openid) einen fehlerhaften Authorize-URL → AADSTS900561. getIntegrationAuthUrl und handleIntegrationCallback verwenden jetzt direkte fetch-Aufrufe (analog zu refreshIntegrationToken) ohne MSAL, was den Fehler umgeht und denselben Standard-OAuth2-Flow garantiert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
254d00c106
commit
1f6e59d362
1 changed files with 67 additions and 31 deletions
|
|
@ -332,65 +332,101 @@ export class EntraIdService implements OnModuleInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorization-URL fuer MS365 Integration generieren.
|
* 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 state CSRF-Token
|
||||||
* @param redirectUri Optionaler Override (fuer dynamischen Host-basierten URI)
|
* @param redirectUri Optionaler Override (fuer dynamischen Host-basierten URI)
|
||||||
*/
|
*/
|
||||||
async getIntegrationAuthUrl(state: string, redirectUri?: string): Promise<string> {
|
async getIntegrationAuthUrl(state: string, redirectUri?: string): Promise<string> {
|
||||||
if (!this.msalClient) {
|
const ssoConfig = await this.loadConfigFromRedis();
|
||||||
|
const tenantId =
|
||||||
|
ssoConfig?.tenantId ?? this.config.get<string>('AZURE_TENANT_ID') ?? '';
|
||||||
|
const clientId =
|
||||||
|
ssoConfig?.clientId ?? this.config.get<string>('AZURE_CLIENT_ID') ?? '';
|
||||||
|
|
||||||
|
if (!tenantId || !clientId) {
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
'Microsoft SSO ist nicht konfiguriert',
|
'Microsoft SSO ist nicht konfiguriert',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authUrlRequest: AuthorizationUrlRequest = {
|
const params = new URLSearchParams({
|
||||||
scopes: this.integrationScopes,
|
client_id: clientId,
|
||||||
redirectUri: redirectUri || this.integrationRedirectUri,
|
response_type: 'code',
|
||||||
|
redirect_uri: redirectUri || this.integrationRedirectUri,
|
||||||
|
scope: this.integrationScopes.join(' '),
|
||||||
state,
|
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.
|
* Authorization Code gegen M365-Tokens tauschen (Standard OAuth2 POST).
|
||||||
* Gibt Access-Token, Refresh-Token und Ablaufzeit zurueck.
|
* Direkte fetch-Implementierung analog zu refreshIntegrationToken —
|
||||||
|
* umgeht MSAL v5 Probleme mit acquireTokenByCode fuer Graph-Scopes.
|
||||||
* @param code Authorization Code von Microsoft
|
* @param code Authorization Code von Microsoft
|
||||||
* @param redirectUri Muss exakt mit dem URI aus dem Auth-Request uebereinstimmen
|
* @param redirectUri Muss exakt mit dem URI aus dem Auth-Request uebereinstimmen
|
||||||
*/
|
*/
|
||||||
async handleIntegrationCallback(code: string, redirectUri?: string): Promise<M365TokenResult> {
|
async handleIntegrationCallback(code: string, redirectUri?: string): Promise<M365TokenResult> {
|
||||||
if (!this.msalClient) {
|
const ssoConfig = await this.loadConfigFromRedis();
|
||||||
|
const tenantId =
|
||||||
|
ssoConfig?.tenantId ?? this.config.get<string>('AZURE_TENANT_ID') ?? '';
|
||||||
|
const clientId =
|
||||||
|
ssoConfig?.clientId ?? this.config.get<string>('AZURE_CLIENT_ID') ?? '';
|
||||||
|
const clientSecret =
|
||||||
|
ssoConfig?.clientSecret ??
|
||||||
|
this.config.get<string>('AZURE_CLIENT_SECRET') ??
|
||||||
|
'';
|
||||||
|
|
||||||
|
if (!tenantId || !clientId || !clientSecret) {
|
||||||
throw new ServiceUnavailableException(
|
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,
|
code,
|
||||||
scopes: this.integrationScopes,
|
redirect_uri: redirectUri || this.integrationRedirectUri,
|
||||||
redirectUri: redirectUri || this.integrationRedirectUri,
|
grant_type: 'authorization_code',
|
||||||
};
|
scope: this.integrationScopes.join(' '),
|
||||||
|
});
|
||||||
|
|
||||||
const response: AuthenticationResult =
|
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
||||||
await this.msalClient.acquireTokenByCode(tokenRequest);
|
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');
|
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 {
|
return {
|
||||||
accessToken: response.accessToken,
|
accessToken: data.access_token,
|
||||||
refreshToken,
|
refreshToken: data.refresh_token ?? '',
|
||||||
expiresAt: response.expiresOn ?? new Date(Date.now() + 3600 * 1000),
|
expiresAt: new Date(Date.now() + data.expires_in * 1000),
|
||||||
scopes: this.integrationScopes,
|
scopes: this.integrationScopes,
|
||||||
msTenantId: tenantId,
|
msTenantId: tenantId,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue