From bc156e1657c08b332443e2827dbe3ebb0e0606d5 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Mon, 9 Mar 2026 22:51:01 +0100 Subject: [PATCH] feat: dynamic SSO configuration via Admin UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSO config (Tenant ID, Client ID, Client Secret, Redirect URI) can now be managed entirely from the Admin SSO page. Config is stored in Redis (persistent) and the MSAL client is reinitialized on save — no server restart or console access required. Co-Authored-By: Claude Opus 4.6 --- .../src/core/auth/sso/entra-id.service.ts | 183 ++++- .../src/core/auth/sso/sso.controller.ts | 90 ++- packages/frontend/src/admin/AdminSsoPage.tsx | 689 +++++++++++++----- 3 files changed, 763 insertions(+), 199 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 0c354e4..26606f5 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 @@ -1,6 +1,7 @@ import { Injectable, Logger, + OnModuleInit, ServiceUnavailableException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -11,6 +12,7 @@ import { type AuthorizationCodeRequest, type AuthenticationResult, } from '@azure/msal-node'; +import { RedisService } from '../../../redis/redis.service'; /** * Informationen aus dem Microsoft ID-Token. @@ -26,53 +28,184 @@ export interface MsUserInfo { lastName: string; } +/** + * SSO-Konfiguration die in Redis gespeichert wird. + */ +export interface SsoConfig { + tenantId: string; + clientId: string; + clientSecret: string; + redirectUri: string; +} + +/** Redis-Key fuer die SSO-Konfiguration (persistent, kein TTL) */ +const SSO_CONFIG_KEY = 'sso_config'; + /** * EntraIdService - Microsoft Entra ID (Azure AD) Integration. * * Nutzt MSAL ConfidentialClientApplication fuer den * Authorization Code Flow (Server-seitig). + * + * Konfiguration wird aus Redis geladen (dynamisch via Admin-UI), + * mit Fallback auf Umgebungsvariablen. */ @Injectable() -export class EntraIdService { +export class EntraIdService implements OnModuleInit { private readonly logger = new Logger(EntraIdService.name); private msalClient: ConfidentialClientApplication | null = null; - private readonly redirectUri: string; + private redirectUri = ''; private readonly scopes = ['openid', 'profile', 'email', 'User.Read']; - constructor(private readonly config: ConfigService) { + constructor( + private readonly config: ConfigService, + private readonly redis: RedisService, + ) {} + + /** + * Beim Start: Konfiguration aus Redis laden (Fallback: Env-Vars). + */ + async onModuleInit(): Promise { + // Versuche Konfiguration aus Redis zu laden + const redisConfig = await this.loadConfigFromRedis(); + + if (redisConfig) { + this.initializeMsal(redisConfig); + this.logger.log( + 'Microsoft Entra ID SSO aus Redis-Konfiguration initialisiert', + ); + return; + } + + // Fallback: Umgebungsvariablen const clientId = this.config.get('AZURE_CLIENT_ID'); const tenantId = this.config.get('AZURE_TENANT_ID'); const clientSecret = this.config.get('AZURE_CLIENT_SECRET'); - this.redirectUri = this.config.get( - 'AZURE_REDIRECT_URI', - 'http://localhost/api/v1/auth/sso/microsoft/callback', - ); + const redirectUri = this.config.get('AZURE_REDIRECT_URI'); if (clientId && tenantId && clientSecret) { - const msalConfig: Configuration = { - auth: { - clientId, - authority: `https://login.microsoftonline.com/${tenantId}`, - clientSecret, - }, - system: { - loggerOptions: { - loggerCallback: (level, message) => { - this.logger.debug(`MSAL [${level}]: ${message}`); - }, - }, - }, - }; - - this.msalClient = new ConfidentialClientApplication(msalConfig); - this.logger.log('Microsoft Entra ID SSO konfiguriert'); + this.initializeMsal({ + tenantId, + clientId, + clientSecret, + redirectUri: + redirectUri || + 'http://localhost/api/v1/auth/sso/microsoft/callback', + }); + this.logger.log( + 'Microsoft Entra ID SSO aus Umgebungsvariablen initialisiert', + ); } else { this.logger.warn( - 'Microsoft Entra ID SSO nicht konfiguriert (AZURE_CLIENT_ID, AZURE_TENANT_ID oder AZURE_CLIENT_SECRET fehlt)', + 'Microsoft Entra ID SSO nicht konfiguriert (weder Redis noch Umgebungsvariablen)', ); } } + /** + * MSAL Client mit der gegebenen Konfiguration initialisieren. + */ + private initializeMsal(ssoConfig: SsoConfig): void { + const msalConfig: Configuration = { + auth: { + clientId: ssoConfig.clientId, + authority: `https://login.microsoftonline.com/${ssoConfig.tenantId}`, + clientSecret: ssoConfig.clientSecret, + }, + system: { + loggerOptions: { + loggerCallback: (level, message) => { + this.logger.debug(`MSAL [${level}]: ${message}`); + }, + }, + }, + }; + + this.msalClient = new ConfidentialClientApplication(msalConfig); + this.redirectUri = ssoConfig.redirectUri; + } + + /** + * Konfiguration aus Redis laden. + */ + private async loadConfigFromRedis(): Promise { + try { + const raw = await this.redis.get(SSO_CONFIG_KEY); + if (!raw) return null; + + const config = JSON.parse(raw) as SsoConfig; + if (config.tenantId && config.clientId && config.clientSecret) { + return config; + } + return null; + } catch (err) { + this.logger.warn( + `Fehler beim Laden der SSO-Config aus Redis: ${(err as Error).message}`, + ); + return null; + } + } + + /** + * Konfiguration speichern und MSAL Client neu initialisieren. + * Wird vom Admin-UI aufgerufen. + */ + async reconfigure(ssoConfig: SsoConfig): Promise { + // In Redis speichern (persistent, kein TTL) + await this.redis.set(SSO_CONFIG_KEY, JSON.stringify(ssoConfig)); + + // MSAL Client neu initialisieren + this.initializeMsal(ssoConfig); + + this.logger.log('Microsoft Entra ID SSO neu konfiguriert via Admin-UI'); + } + + /** + * Aktuelle Konfiguration lesen (Secret wird maskiert). + */ + async getConfig(): Promise< + | (Omit & { clientSecretMasked: string }) + | null + > { + // Zuerst aus Redis + const redisConfig = await this.loadConfigFromRedis(); + if (redisConfig) { + return { + tenantId: redisConfig.tenantId, + clientId: redisConfig.clientId, + redirectUri: redisConfig.redirectUri, + clientSecretMasked: this.maskSecret(redisConfig.clientSecret), + }; + } + + // Fallback: Env-Vars + const clientId = this.config.get('AZURE_CLIENT_ID'); + const tenantId = this.config.get('AZURE_TENANT_ID'); + const clientSecret = this.config.get('AZURE_CLIENT_SECRET'); + const redirectUri = this.config.get('AZURE_REDIRECT_URI'); + + if (clientId && tenantId && clientSecret) { + return { + tenantId, + clientId, + redirectUri: + redirectUri || + 'http://localhost/api/v1/auth/sso/microsoft/callback', + clientSecretMasked: this.maskSecret(clientSecret), + }; + } + + return null; + } + + /** + * Secret maskieren: nur die letzten 4 Zeichen anzeigen. + */ + private maskSecret(secret: string): string { + if (secret.length <= 4) return '****'; + return '****' + secret.slice(-4); + } + /** * Ist Entra ID SSO konfiguriert? */ diff --git a/packages/core-service/src/core/auth/sso/sso.controller.ts b/packages/core-service/src/core/auth/sso/sso.controller.ts index d6b1825..a204e29 100644 --- a/packages/core-service/src/core/auth/sso/sso.controller.ts +++ b/packages/core-service/src/core/auth/sso/sso.controller.ts @@ -1,20 +1,25 @@ import { Controller, Get, + Post, + Body, Query, Res, Logger, ServiceUnavailableException, UnauthorizedException, - InternalServerErrorException, + UseGuards, + BadRequestException, } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { Public } from '../../../common/decorators/public.decorator'; +import { Roles } from '../../../common/decorators/roles.decorator'; +import { RolesGuard } from '../../../common/guards/roles.guard'; import { RedisService } from '../../../redis/redis.service'; -import { EntraIdService } from './entra-id.service'; +import { EntraIdService, type SsoConfig } from './entra-id.service'; import { AuthService } from '../auth.service'; /** @@ -23,6 +28,10 @@ import { AuthService } from '../auth.service'; * Flow: * 1. GET /auth/sso/microsoft → Redirect zu Microsoft Login * 2. GET /auth/sso/microsoft/callback → Callback von Microsoft, User anlegen/verknuepfen, JWT generieren + * + * Admin: + * - GET /auth/sso/config → Aktuelle Konfiguration lesen (Secret maskiert) + * - POST /auth/sso/config → Konfiguration speichern und MSAL neu initialisieren */ @ApiTags('SSO') @Controller('auth/sso') @@ -36,6 +45,83 @@ export class SsoController { private readonly config: ConfigService, ) {} + /** + * GET /api/v1/auth/sso/config + * Aktuelle SSO-Konfiguration lesen (nur PLATFORM_ADMIN). + * Client Secret wird maskiert zurueckgegeben. + */ + @Get('config') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'SSO-Konfiguration lesen (Admin)' }) + async getConfig() { + const config = await this.entraIdService.getConfig(); + return { + configured: this.entraIdService.isConfigured(), + config: config || null, + }; + } + + /** + * POST /api/v1/auth/sso/config + * SSO-Konfiguration speichern und MSAL Client neu initialisieren (nur PLATFORM_ADMIN). + */ + @Post('config') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'SSO-Konfiguration speichern (Admin)' }) + async saveConfig( + @Body() + body: { + tenantId: string; + clientId: string; + clientSecret?: string; + redirectUri: string; + }, + ) { + // Validierung + if (!body.tenantId || !body.clientId || !body.redirectUri) { + throw new BadRequestException( + 'Tenant ID, Client ID und Redirect URI sind erforderlich', + ); + } + + // Wenn kein neues Secret: bestehendes aus Redis laden + let clientSecret = body.clientSecret; + if (!clientSecret) { + const existing = await this.entraIdService.getConfig(); + // Wir brauchen das echte Secret aus Redis + const raw = await this.redis.get('sso_config'); + if (raw) { + const parsed = JSON.parse(raw) as SsoConfig; + clientSecret = parsed.clientSecret; + } + } + + if (!clientSecret) { + throw new BadRequestException( + 'Client Secret ist erforderlich (kein bestehendes Secret vorhanden)', + ); + } + + const ssoConfig: SsoConfig = { + tenantId: body.tenantId.trim(), + clientId: body.clientId.trim(), + clientSecret: clientSecret.trim(), + redirectUri: body.redirectUri.trim(), + }; + + await this.entraIdService.reconfigure(ssoConfig); + + this.logger.log('SSO-Konfiguration via Admin-UI aktualisiert'); + + return { + success: true, + message: 'SSO-Konfiguration gespeichert und aktiviert', + configured: true, + }; + } + /** * GET /api/v1/auth/sso/microsoft * Initiiert den OAuth2 Authorization Code Flow. diff --git a/packages/frontend/src/admin/AdminSsoPage.tsx b/packages/frontend/src/admin/AdminSsoPage.tsx index 5b44776..5f19cce 100644 --- a/packages/frontend/src/admin/AdminSsoPage.tsx +++ b/packages/frontend/src/admin/AdminSsoPage.tsx @@ -1,10 +1,21 @@ -import { useQuery } from '@tanstack/react-query'; +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../api/client'; interface SsoStatus { microsoft: boolean; } +interface SsoConfigResponse { + configured: boolean; + config: { + tenantId: string; + clientId: string; + redirectUri: string; + clientSecretMasked: string; + } | null; +} + const cardStyle: React.CSSProperties = { background: 'var(--color-bg-card)', borderRadius: 'var(--radius-md)', @@ -14,19 +25,6 @@ const cardStyle: React.CSSProperties = { marginBottom: '1.5rem', }; -const codeBlockStyle: React.CSSProperties = { - background: '#1e293b', - color: '#e2e8f0', - borderRadius: 'var(--radius-sm)', - padding: '1rem 1.25rem', - fontSize: '0.8125rem', - fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace", - lineHeight: 1.7, - overflowX: 'auto', - whiteSpace: 'pre', - marginTop: '0.75rem', -}; - const inlineCodeStyle: React.CSSProperties = { background: 'var(--color-bg)', padding: '0.125rem 0.375rem', @@ -98,8 +96,37 @@ const tdStyle: React.CSSProperties = { verticalAlign: 'top', }; +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '0.625rem 0.75rem', + fontSize: '0.875rem', + border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-sm)', + background: 'var(--color-bg-card)', + color: 'var(--color-text)', + fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace", + outline: 'none', + boxSizing: 'border-box' as const, +}; + +const labelStyle: React.CSSProperties = { + display: 'block', + fontSize: '0.8125rem', + fontWeight: 600, + color: 'var(--color-text)', + marginBottom: '0.375rem', +}; + +const hintStyle: React.CSSProperties = { + fontSize: '0.75rem', + color: 'var(--color-text-muted)', + marginTop: '0.25rem', +}; + export function AdminSsoPage() { - const { data: ssoStatus, isLoading } = useQuery({ + const queryClient = useQueryClient(); + + const { data: ssoStatus, isLoading: statusLoading } = useQuery({ queryKey: ['sso', 'status'], queryFn: async () => { const res = await api.get('/auth/sso/status'); @@ -107,48 +134,156 @@ export function AdminSsoPage() { }, }); - const redirectUri = `${window.location.origin}/api/v1/auth/sso/microsoft/callback`; + const { data: ssoConfig, isLoading: configLoading } = + useQuery({ + queryKey: ['sso', 'config'], + queryFn: async () => { + const res = await api.get('/auth/sso/config'); + return res.data; + }, + }); + + // Formular-State + const [tenantId, setTenantId] = useState(''); + const [clientId, setClientId] = useState(''); + const [clientSecret, setClientSecret] = useState(''); + const [redirectUri, setRedirectUri] = useState(''); + const [saveSuccess, setSaveSuccess] = useState(false); + + // Default Redirect-URI + const defaultRedirectUri = `${window.location.origin}/api/v1/auth/sso/microsoft/callback`; + + // Formular mit bestehenden Werten befuellen + useEffect(() => { + if (ssoConfig?.config) { + setTenantId(ssoConfig.config.tenantId); + setClientId(ssoConfig.config.clientId); + setRedirectUri(ssoConfig.config.redirectUri); + // Secret bleibt leer — wird nur bei Aenderung gesendet + } else { + // Default Redirect-URI setzen wenn noch keine Config da + setRedirectUri(defaultRedirectUri); + } + }, [ssoConfig, defaultRedirectUri]); + + const saveMutation = useMutation({ + mutationFn: async (data: { + tenantId: string; + clientId: string; + clientSecret?: string; + redirectUri: string; + }) => { + const res = await api.post('/auth/sso/config', data); + return res.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sso', 'status'] }); + queryClient.invalidateQueries({ queryKey: ['sso', 'config'] }); + setClientSecret(''); // Secret-Feld leeren + setSaveSuccess(true); + setTimeout(() => setSaveSuccess(false), 4000); + }, + }); + + const handleSave = () => { + const data: { + tenantId: string; + clientId: string; + clientSecret?: string; + redirectUri: string; + } = { + tenantId, + clientId, + redirectUri, + }; + + // Secret nur mitsenden wenn es geaendert wurde + if (clientSecret) { + data.clientSecret = clientSecret; + } + + saveMutation.mutate(data); + }; + + const isLoading = statusLoading || configLoading; + const hasExistingConfig = !!ssoConfig?.config; + const canSave = + tenantId.trim() && + clientId.trim() && + redirectUri.trim() && + (clientSecret.trim() || hasExistingConfig); // Secret nur noetig bei Erstconfig return (
-
-

SSO-Konfiguration

+
+

+ SSO-Konfiguration +

{/* Status-Anzeige */}
-
- +
+ -

Microsoft Entra ID (Azure AD)

+

+ Microsoft Entra ID (Azure AD) +

-
- - +
+ + {isLoading ? 'Status wird geladen...' : ssoStatus?.microsoft @@ -160,7 +295,13 @@ export function AdminSsoPage() { {/* Einrichtungs-Anleitung */}
-

+

Einrichtungsanleitung

@@ -175,11 +316,14 @@ export function AdminSsoPage() { href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade" target="_blank" rel="noopener noreferrer" - style={{ color: 'var(--color-primary)', textDecoration: 'underline' }} + style={{ + color: 'var(--color-primary)', + textDecoration: 'underline', + }} > Azure Portal → App registrations - - {' '}und klicke auf New registration. + {' '} + und klicke auf New registration.

@@ -190,23 +334,37 @@ export function AdminSsoPage() { - + - + - + @@ -220,7 +378,9 @@ export function AdminSsoPage() {

Client Secret erstellen

- In der App Registration → Certificates & secretsNew client secret. + In der App Registration →{' '} + Certificates & secrets →{' '} + New client secret.

Name + Name + INSIGHT Platform
Supported account types - Accounts in this organizational directory only
- + Supported account types +
+ Accounts in this organizational directory only +
+ (Single tenant — nur euer Azure AD)
Redirect URI - Platform: Web
- URI: {redirectUri} + Redirect URI +
+ Platform: Web +
+ URI:{' '} + {defaultRedirectUri}
@@ -231,30 +391,43 @@ export function AdminSsoPage() { - + - +
Description + Description + INSIGHT Platform SSO
Expires - 24 months (empfohlen)
- + Expires +
+ 24 months (empfohlen) +
+ Danach muss das Secret erneuert werden
-
- Wichtig: Den Value des Secrets sofort kopieren — er wird nur einmalig angezeigt! +
+ Wichtig: Den Value des Secrets + sofort kopieren — er wird nur einmalig angezeigt!
@@ -265,7 +438,11 @@ export function AdminSsoPage() {

API Permissions konfigurieren

- In der App Registration → API permissionsAdd a permissionMicrosoft GraphDelegated permissions. + In der App Registration →{' '} + API permissions →{' '} + Add a permission →{' '} + Microsoft Graph →{' '} + Delegated permissions.

Folgende Berechtigungen hinzufügen:

@@ -278,133 +455,261 @@ export function AdminSsoPage() { - + - + - + - +
openid + openid + Delegated Sign users in
profile + profile + Delegated View users' basic profile
email + email + Delegated View users' email address
User.Read + User.Read + Delegated Sign in and read user profile

- Dann Grant admin consent klicken, um die Berechtigungen für alle Benutzer freizugeben. + Dann Grant admin consent klicken, um die + Berechtigungen für alle Benutzer freizugeben.

- {/* Schritt 4 */} + {/* Schritt 4 — Konfiguration eingeben und speichern */}
4
-

Werte aus Azure Portal kopieren

+

+ Werte aus Azure Portal hier eintragen und speichern +

- In der App Registration → Overview findest du die benötigten Werte: + Kopiere die Werte aus der App Registration →{' '} + Overview und trage sie in die Felder ein. Beim + Speichern wird die SSO-Verbindung automatisch aktiviert.

- - - - - - - - - - - - - - - - - - - - - -
Azure Portal FeldUmgebungsvariable
Directory (tenant) IDAZURE_TENANT_ID
Application (client) IDAZURE_CLIENT_ID
Client secret value (aus Schritt 2)AZURE_CLIENT_SECRET
-
-
- {/* Schritt 5 */} -
-
5
-
-

Umgebungsvariablen auf dem Server setzen

-

- Trage die Werte in die .env-Datei auf dem Server ein: -

-
-{`AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -AZURE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -AZURE_REDIRECT_URI=${redirectUri}`} -
-
-
+
+ {/* Tenant ID */} +
+ + setTenantId(e.target.value)} + placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + style={inputStyle} + /> +
+ Azure Portal → App Registration → Overview → + Directory (tenant) ID +
+
- {/* Schritt 6 */} -
-
6
-
-

Core-Service neu starten

-

- Nach dem Setzen der Umgebungsvariablen den Backend-Service neu starten: -

-
-{`docker compose restart core`} + {/* Client ID */} +
+ + setClientId(e.target.value)} + placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + style={inputStyle} + /> +
+ Azure Portal → App Registration → Overview → + Application (client) ID +
+
+ + {/* Client Secret */} +
+ + setClientSecret(e.target.value)} + placeholder={ + hasExistingConfig + ? `Gespeichert (${ssoConfig?.config?.clientSecretMasked}) — leer lassen um beizubehalten` + : 'Client Secret Value eingeben' + } + style={inputStyle} + /> +
+ Azure Portal → App Registration → Certificates & + secrets → Client secret Value + {hasExistingConfig && ( + + {' '} + — Leer lassen, um das bestehende Secret zu behalten + + )} +
+
+ + {/* Redirect URI */} +
+ + setRedirectUri(e.target.value)} + placeholder={defaultRedirectUri} + style={inputStyle} + /> +
+ Muss exakt mit der Redirect URI in der Azure App Registration + übereinstimmen +
+
+ + {/* Speichern-Button + Status */} +
+ + + {saveSuccess && ( + + SSO-Konfiguration gespeichert und aktiviert! + + )} + + {saveMutation.isError && ( + + Fehler:{' '} + {(saveMutation.error as Error)?.message || + 'Konfiguration konnte nicht gespeichert werden'} + + )} +
-

- Nach dem Neustart sollte der Status oben auf{' '} - aktiv wechseln und - der „Mit Microsoft anmelden"-Button auf der Login-Seite erscheinen. -

{/* Funktionsweise */}
-

+

Funktionsweise

-
-
+
+

Erstanmeldung

-

+

Wenn ein Benutzer sich zum ersten Mal mit Microsoft anmeldet, - wird automatisch ein Konto angelegt (Rolle: Benutzer). - Existiert bereits ein Konto mit derselben E-Mail-Adresse, - wird das Microsoft-Konto automatisch verknüpft. + wird automatisch ein Konto angelegt (Rolle: Benutzer). Existiert + bereits ein Konto mit derselben E-Mail-Adresse, wird das + Microsoft-Konto automatisch verknüpft.

-
+

Sicherheit

-

- Der SSO-Flow nutzt den OAuth2 Authorization Code Flow. - Das Client Secret verlässt nie den Server. - CSRF-Schutz via State-Parameter. - Die Multi-Faktor-Authentifizierung (MFA) von Microsoft wird unterstützt. +

+ Der SSO-Flow nutzt den OAuth2 Authorization Code Flow. Das Client + Secret verlässt nie den Server. CSRF-Schutz via State-Parameter. + Die Multi-Faktor-Authentifizierung (MFA) von Microsoft wird + unterstützt.

@@ -412,7 +717,13 @@ AZURE_REDIRECT_URI=${redirectUri}`} {/* Technische Details */}
-

+

Technische Referenz

@@ -424,34 +735,68 @@ AZURE_REDIRECT_URI=${redirectUri}`} - - + + - - + + - - + +
GET /api/v1/auth/sso/microsoftStartet den SSO-Flow (Redirect zu Microsoft) + + GET /api/v1/auth/sso/microsoft + + + Startet den SSO-Flow (Redirect zu Microsoft) +
GET /api/v1/auth/sso/microsoft/callbackCallback von Microsoft (Token-Exchange + User-Provisioning) + + GET /api/v1/auth/sso/microsoft/callback + + + Callback von Microsoft (Token-Exchange + User-Provisioning) +
GET /api/v1/auth/sso/statusPrüft ob SSO konfiguriert ist (für Login-Seite) + + GET /api/v1/auth/sso/status + + + Prüft ob SSO konfiguriert ist (für Login-Seite) +
-

Redirect URI

+

+ Redirect URI +

- Diese URI muss exakt so in der Azure App Registration eingetragen sein: + Diese URI muss exakt so in der Azure App Registration eingetragen + sein:

-
- {redirectUri} +
+ {defaultRedirectUri}