deploy
This commit is contained in:
@@ -16,7 +16,9 @@ $auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
|||||||
$accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration'));
|
$accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration'));
|
||||||
$state = (string) ($_GET['state'] ?? '');
|
$state = (string) ($_GET['state'] ?? '');
|
||||||
$expectedState = (string) ($_SESSION['keycloak_oauth_state'] ?? '');
|
$expectedState = (string) ($_SESSION['keycloak_oauth_state'] ?? '');
|
||||||
|
$oauthMode = (string) ($_SESSION['keycloak_oauth_mode'] ?? 'interactive');
|
||||||
$code = (string) ($_GET['code'] ?? '');
|
$code = (string) ($_GET['code'] ?? '');
|
||||||
|
$error = trim((string) ($_GET['error'] ?? ''));
|
||||||
|
|
||||||
$title = 'Keycloak Callback';
|
$title = 'Keycloak Callback';
|
||||||
$message = 'Anmeldung wird verarbeitet.';
|
$message = 'Anmeldung wird verarbeitet.';
|
||||||
@@ -25,11 +27,21 @@ $redirectTarget = '/';
|
|||||||
if ($state === '' || $expectedState === '' || !hash_equals($expectedState, $state)) {
|
if ($state === '' || $expectedState === '' || !hash_equals($expectedState, $state)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
$message = 'State-Pruefung fehlgeschlagen. Bitte den Login erneut starten.';
|
$message = 'State-Pruefung fehlgeschlagen. Bitte den Login erneut starten.';
|
||||||
|
} elseif ($error !== '') {
|
||||||
|
unset($_SESSION['keycloak_oauth_state'], $_SESSION['keycloak_oauth_mode']);
|
||||||
|
|
||||||
|
if ($oauthMode === 'silent' && in_array($error, ['login_required', 'interaction_required', 'consent_required'], true)) {
|
||||||
|
header('Location: ' . $redirectTarget, true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
$message = 'Keycloak hat die Anmeldung abgelehnt: ' . $error . '.';
|
||||||
} elseif ($code === '') {
|
} elseif ($code === '') {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
$message = 'Kein Authorization Code von Keycloak erhalten.';
|
$message = 'Kein Authorization Code von Keycloak erhalten.';
|
||||||
} else {
|
} else {
|
||||||
unset($_SESSION['keycloak_oauth_state']);
|
unset($_SESSION['keycloak_oauth_state'], $_SESSION['keycloak_oauth_mode']);
|
||||||
|
|
||||||
$tokenExchange = $auth->exchangeAuthorizationCode($code);
|
$tokenExchange = $auth->exchangeAuthorizationCode($code);
|
||||||
|
|
||||||
@@ -67,6 +79,7 @@ if ($state === '' || $expectedState === '' || !hash_equals($expectedState, $stat
|
|||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$auth->establishSession($tokenPayload, $userPayload);
|
$auth->establishSession($tokenPayload, $userPayload);
|
||||||
|
unset($_SESSION['desktop_silent_sso_attempted']);
|
||||||
header('Location: ' . $redirectTarget, true, 302);
|
header('Location: ' . $redirectTarget, true, 302);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ session_start();
|
|||||||
|
|
||||||
$projectRoot = dirname(__DIR__, 3);
|
$projectRoot = dirname(__DIR__, 3);
|
||||||
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
||||||
$loginUrl = $auth->loginUrl();
|
$mode = trim((string) ($_GET['mode'] ?? ''));
|
||||||
|
$loginUrl = $auth->loginUrl($mode === 'silent' ? ['prompt' => 'none'] : []);
|
||||||
|
|
||||||
if ($loginUrl === null) {
|
if ($loginUrl === null) {
|
||||||
http_response_code(503);
|
http_response_code(503);
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ if ($auth->isAuthenticated()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$auth->isAuthenticated()
|
||||||
|
&& $auth->isConfigured()
|
||||||
|
&& empty($_SESSION['desktop_silent_sso_attempted'])
|
||||||
|
) {
|
||||||
|
$_SESSION['desktop_silent_sso_attempted'] = true;
|
||||||
|
header('Location: /auth/keycloak/?mode=silent', true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$app = new App($projectRoot);
|
$app = new App($projectRoot);
|
||||||
$desktopPayload = $app->desktopPayload();
|
$desktopPayload = $app->desktopPayload();
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ final class KeycloakAuth
|
|||||||
return $this->allowDesktopPreview() && isset($_GET[$previewKey]);
|
return $this->allowDesktopPreview() && isset($_GET[$previewKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loginUrl(): ?string
|
/**
|
||||||
|
* @param array<string, string> $authorizeOverrides
|
||||||
|
*/
|
||||||
|
public function loginUrl(array $authorizeOverrides = []): ?string
|
||||||
{
|
{
|
||||||
if (!$this->isConfigured()) {
|
if (!$this->isConfigured()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -70,6 +73,7 @@ final class KeycloakAuth
|
|||||||
|
|
||||||
$state = bin2hex(random_bytes(16));
|
$state = bin2hex(random_bytes(16));
|
||||||
$_SESSION['keycloak_oauth_state'] = $state;
|
$_SESSION['keycloak_oauth_state'] = $state;
|
||||||
|
$_SESSION['keycloak_oauth_mode'] = (string) ($authorizeOverrides['prompt'] ?? '') === 'none' ? 'silent' : 'interactive';
|
||||||
|
|
||||||
$params = array_merge(
|
$params = array_merge(
|
||||||
[
|
[
|
||||||
@@ -83,6 +87,7 @@ final class KeycloakAuth
|
|||||||
? $this->config['extra_authorize_params']
|
? $this->config['extra_authorize_params']
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
$params = array_merge($params, $authorizeOverrides);
|
||||||
|
|
||||||
return rtrim($this->baseUrl(), '/')
|
return rtrim($this->baseUrl(), '/')
|
||||||
. '/realms/' . rawurlencode($this->realm())
|
. '/realms/' . rawurlencode($this->realm())
|
||||||
|
|||||||
@@ -257,6 +257,10 @@
|
|||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-inline-access-trigger {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
#admin-apps-app .aa-inline-access-current {
|
#admin-apps-app .aa-inline-access-current {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -326,6 +330,65 @@
|
|||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-secondary {
|
||||||
|
background: rgba(226, 232, 240, 0.9);
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal {
|
||||||
|
width: min(680px, calc(100vw - 40px));
|
||||||
|
max-height: calc(100vh - 40px);
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: 0 22px 70px rgba(15, 23, 42, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal::backdrop {
|
||||||
|
background: rgba(15, 23, 42, 0.42);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 22px;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.96));
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.15;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin-apps-app .aa-modal-close {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(226, 232, 240, 0.9);
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
#admin-apps-app .aa-message.is-success {
|
#admin-apps-app .aa-message.is-success {
|
||||||
background: rgba(34, 197, 94, 0.12);
|
background: rgba(34, 197, 94, 0.12);
|
||||||
color: #166534;
|
color: #166534;
|
||||||
|
|||||||
@@ -22,6 +22,54 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const bindInteractions = (host, loadRoute) => {
|
const bindInteractions = (host, loadRoute) => {
|
||||||
|
host.querySelectorAll('[data-aa-open-modal]').forEach((button) => {
|
||||||
|
if (button.dataset.aaBound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.dataset.aaBound = '1';
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const modalId = button.getAttribute('data-aa-open-modal') || '';
|
||||||
|
const modal = host.querySelector(`[data-aa-modal="${CSS.escape(modalId)}"]`);
|
||||||
|
if (modal instanceof HTMLDialogElement) {
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
host.querySelectorAll('[data-aa-close-modal]').forEach((button) => {
|
||||||
|
if (button.dataset.aaBound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.dataset.aaBound = '1';
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const modal = button.closest('dialog');
|
||||||
|
if (modal instanceof HTMLDialogElement) {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
host.querySelectorAll('dialog[data-aa-modal]').forEach((dialog) => {
|
||||||
|
if (dialog.dataset.aaBound === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.dataset.aaBound = '1';
|
||||||
|
dialog.addEventListener('click', (event) => {
|
||||||
|
const rect = dialog.getBoundingClientRect();
|
||||||
|
const inside = event.clientX >= rect.left
|
||||||
|
&& event.clientX <= rect.right
|
||||||
|
&& event.clientY >= rect.top
|
||||||
|
&& event.clientY <= rect.bottom;
|
||||||
|
|
||||||
|
if (!inside) {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
host.querySelectorAll('a[href]').forEach((link) => {
|
host.querySelectorAll('a[href]').forEach((link) => {
|
||||||
if (link.dataset.aaBound === '1') {
|
if (link.dataset.aaBound === '1') {
|
||||||
return;
|
return;
|
||||||
@@ -66,6 +114,11 @@
|
|||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modal = form.closest('dialog');
|
||||||
|
if (modal instanceof HTMLDialogElement) {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
|
||||||
host.innerHTML = html;
|
host.innerHTML = html;
|
||||||
bindInteractions(host, loadRoute);
|
bindInteractions(host, loadRoute);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -289,13 +289,33 @@ ob_start();
|
|||||||
<td><?= $h((string) ($app['app_scope'] ?? '')) ?></td>
|
<td><?= $h((string) ($app['app_scope'] ?? '')) ?></td>
|
||||||
<td><?= $h(!empty($app['installable']) ? 'Custom App' : 'System') ?></td>
|
<td><?= $h(!empty($app['installable']) ? 'Custom App' : 'System') ?></td>
|
||||||
<td>
|
<td>
|
||||||
<form class="aa-inline-access-form" method="post" action="/admin/apps/">
|
<div class="aa-inline-access-form">
|
||||||
|
<p class="aa-inline-access-current"><?= $h((string) ($app['required_groups_display'] ?? '')) ?></p>
|
||||||
|
<button
|
||||||
|
class="window-app-button aa-inline-access-trigger"
|
||||||
|
type="button"
|
||||||
|
data-aa-open-modal="access-<?= $h((string) ($app['app_id'] ?? '')) ?>"
|
||||||
|
>Berechtigungen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog class="aa-modal" data-aa-modal="access-<?= $h((string) ($app['app_id'] ?? '')) ?>">
|
||||||
|
<form class="aa-modal-card" method="post" action="/admin/apps/">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $h($csrfToken) ?>">
|
<input type="hidden" name="csrf_token" value="<?= $h($csrfToken) ?>">
|
||||||
<input type="hidden" name="action" value="save-app-access">
|
<input type="hidden" name="action" value="save-app-access">
|
||||||
<input type="hidden" name="active_section" value="overview">
|
<input type="hidden" name="active_section" value="overview">
|
||||||
<input type="hidden" name="app_id" value="<?= $h((string) ($app['app_id'] ?? '')) ?>">
|
<input type="hidden" name="app_id" value="<?= $h((string) ($app['app_id'] ?? '')) ?>">
|
||||||
|
|
||||||
<p class="aa-inline-access-current"><?= $h((string) ($app['required_groups_display'] ?? '')) ?></p>
|
<div class="aa-modal-head">
|
||||||
|
<div>
|
||||||
|
<p class="window-app-kicker aa-kicker">Berechtigungen</p>
|
||||||
|
<h4 class="aa-modal-title"><?= $h((string) ($app['title'] ?? '')) ?></h4>
|
||||||
|
<p class="aa-copy">LDAP-Gruppen und Freigabe fuer den Logoff-Desktop zentral festlegen.</p>
|
||||||
|
</div>
|
||||||
|
<button class="aa-modal-close" type="button" data-aa-close-modal>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aa-modal-body">
|
||||||
|
<p class="aa-inline-access-current">Aktuell: <?= $h((string) ($app['required_groups_display'] ?? '')) ?></p>
|
||||||
<label class="aa-inline-access-toggle">
|
<label class="aa-inline-access-toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -307,8 +327,6 @@ ob_start();
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<?php if ($availableGroups !== []): ?>
|
<?php if ($availableGroups !== []): ?>
|
||||||
<details class="aa-inline-access-details">
|
|
||||||
<summary>Bearbeiten</summary>
|
|
||||||
<div class="aa-inline-access-groups">
|
<div class="aa-inline-access-groups">
|
||||||
<?php foreach ($availableGroups as $group): ?>
|
<?php foreach ($availableGroups as $group): ?>
|
||||||
<label class="aa-inline-access-option">
|
<label class="aa-inline-access-option">
|
||||||
@@ -325,12 +343,17 @@ ob_start();
|
|||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<button class="window-app-button aa-primary aa-inline-access-submit" type="submit">Speichern</button>
|
|
||||||
</details>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="aa-table-copy">Keine LDAP-Gruppen gefunden.</p>
|
<p class="aa-table-copy">Keine LDAP-Gruppen gefunden.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aa-actions">
|
||||||
|
<button class="window-app-button aa-secondary" type="button" data-aa-close-modal>Abbrechen</button>
|
||||||
|
<button class="window-app-button aa-primary aa-inline-access-submit" type="submit">Speichern</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</dialog>
|
||||||
</td>
|
</td>
|
||||||
<td><code><?= $h((string) ($app['entry_route'] ?? '')) ?></code></td>
|
<td><code><?= $h((string) ($app['entry_route'] ?? '')) ?></code></td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
Reference in New Issue
Block a user