From 2a319001bc16cabfc937fb7a2d520e27e1837955 Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Wed, 14 Jan 2026 16:40:21 -0500
Subject: [PATCH 1/9] chore: replace graphql server
---
.../snapshots/rc.nginx.modified.snapshot | 2 +-
.../modifications/patches/rc-nginx.patch | 13 +++
.../modifications/rc-nginx.modification.ts | 13 ++-
plugin/plugins/dynamix.unraid.net.plg | 66 +++++++++++++
.../dynamix.unraid.net/etc/rc.d/rc.unraid | 88 +++++++++++++++++
.../etc/rc.d/rc6.d/K30unraid-core | 7 ++
.../dynamix.unraid.net/install/doinst.sh | 2 +-
.../install/scripts/verify_install.sh | 12 ++-
web/__test__/components/SsoButton.test.ts | 98 +------------------
web/src/components/sso/useSsoAuth.ts | 48 +--------
10 files changed, 202 insertions(+), 147 deletions(-)
create mode 100755 plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
create mode 100644 plugin/source/dynamix.unraid.net/etc/rc.d/rc6.d/K30unraid-core
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
index 7c555fda54..cdab15b5d8 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
@@ -420,7 +420,7 @@ build_locations(){
location /graphql {
allow all;
error_log /dev/null crit;
- proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
+ proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
index fbac95e0bc..9eea949545 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
@@ -44,6 +44,19 @@ Index: /etc/rc.d/rc.nginx
T=' '
if check && [[ $1 == lo ]]; then
if [[ $IPV4 == yes ]]; then
+@@ -400,11 +418,11 @@
+ # my servers proxy
+ #
+ location /graphql {
+ allow all;
+ error_log /dev/null crit;
+- proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
++ proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_cache_bypass $http_upgrade;
@@ -566,11 +584,11 @@
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
index 6b1c717a3a..1e21880152 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
@@ -29,9 +29,9 @@ export default class RcNginxModification extends FileModification {
throw new Error(`File ${this.filePath} not found.`);
}
const fileContent = await readFile(this.filePath, 'utf8');
- if (!fileContent.includes('MYSERVERS=')) {
- throw new Error(`MYSERVERS not found in the file; incorrect target?`);
- }
+ // if (!fileContent.includes('MYSERVERS=')) {
+ // throw new Error(`MYSERVERS not found in the file; incorrect target?`);
+ // }
let newContent = fileContent.replace(
'MYSERVERS="/boot/config/plugins/dynamix.my.servers/myservers.cfg"',
@@ -68,6 +68,11 @@ check_remote_access(){
`if [[ -L /usr/local/sbin/unraid-api ]] && check_remote_access; then`
);
+ newContent = newContent.replace(
+ 'proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;',
+ 'proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;'
+ );
+
newContent = newContent.replace(
'for NET in ${!NET_FQDN6[@]}; do',
'for NET in "${!NET_FQDN6[@]}"; do'
@@ -91,7 +96,7 @@ check_remote_access(){
}
async shouldApply(): Promise {
- const { shouldApply, reason } = await super.shouldApply();
+ const { shouldApply, reason } = await super.shouldApply({ checkOsVersion: false });
return {
shouldApply,
reason,
diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index 6e8a60e695..da2b61b70c 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -9,6 +9,10 @@
+
+
+
+
@@ -52,6 +56,12 @@ exit 0
&txz_sha256;
+
+
+ &core_txz_url;
+ &core_txz_sha256;
+
+
@@ -320,6 +330,21 @@ exit 0
fi
fi
+ # Stop and remove Unraid Core package
+ if [ -x "/etc/rc.d/rc.unraid" ]; then
+ echo "Stopping Unraid Core..."
+ /etc/rc.d/rc.unraid stop || echo "Warning: Failed to stop Unraid Core"
+ fi
+
+ core_pkg_installed=$(ls -1 /var/log/packages/unraid-* 2>/dev/null | head -1)
+ if [ -n "$core_pkg_installed" ]; then
+ core_pkg_basename=$(basename "$core_pkg_installed")
+ echo "Removing core package: $core_pkg_basename"
+ removepkg --terse "$core_pkg_basename"
+ else
+ echo "No Unraid Core package found"
+ fi
+
# File restoration function
echo "Restoring files..."
@@ -404,6 +429,9 @@ exit 0
PKG_FILE="&source;" # Full path to the package file including .txz extension
PKG_URL="&txz_url;" # URL where package was downloaded from
PKG_NAME="&txz_name;" # Name of the package file
+ CORE_PKG_FILE="&core_source;"
+ CORE_PKG_URL="&core_txz_url;"
+ CORE_PKG_NAME="&core_txz_name;"
CONNECT_API_VERSION="&api_version;" # Version of API included with Connect
@@ -599,6 +658,13 @@ echo "If no additional messages appear within 30 seconds, it is safe to refresh
/etc/rc.d/rc.unraid-api start
echo "Unraid API service started"
+if [ -x "/etc/rc.d/rc.unraid" ]; then
+ echo "Starting Unraid Core service"
+ /etc/rc.d/rc.unraid start
+ echo "Unraid Core service started"
+else
+ echo "Warning: rc.unraid not found; core service not started"
+fi
echo "✅ Installation is complete, it is safe to close this window"
echo
exit 0
diff --git a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
new file mode 100755
index 0000000000..d7c35b4abd
--- /dev/null
+++ b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
@@ -0,0 +1,88 @@
+#!/bin/bash
+# /etc/rc.d/rc.unraid
+# Unraid Phoenix Application Service
+
+APP_DIR="/usr/local/unraid"
+RELEASE_BIN="$APP_DIR/_build/prod/rel/unraid/bin/unraid"
+CONFIG_DIR="/boot/config/unraid"
+SOCKET_PATH="/var/run/unraid-core.sock"
+LOG_PATH="${UNRAID_LOG_PATH:-/var/log/unraid-core.log}"
+
+# Load user env if exists
+[ -f "$CONFIG_DIR/env" ] && source "$CONFIG_DIR/env"
+
+# Ensure config and log directories exist
+mkdir -p "$CONFIG_DIR"
+mkdir -p "$(dirname "$LOG_PATH")"
+touch "$LOG_PATH"
+
+# Generate secret_key_base if not exists
+if [ ! -f "$CONFIG_DIR/secret_key_base" ]; then
+ head -c 64 /dev/urandom | base64 | tr -d '\n' > "$CONFIG_DIR/secret_key_base"
+ chmod 600 "$CONFIG_DIR/secret_key_base"
+fi
+
+export SECRET_KEY_BASE=$(cat "$CONFIG_DIR/secret_key_base")
+export RELEASE_COOKIE=$(cat "$CONFIG_DIR/secret_key_base" | head -c 20)
+export UNRAID_CONFIG_DIR="$CONFIG_DIR"
+export RUN_ERL_LOG="${RUN_ERL_LOG:-$LOG_PATH}"
+export RELEASE_LOG_DIR="${RELEASE_LOG_DIR:-$(dirname "$LOG_PATH")}"
+export RELEASE_NODE="${UNRAID_RELEASE_NODE:-unraid}"
+export RELEASE_DISTRIBUTION="${UNRAID_RELEASE_DISTRIBUTION:-sname}"
+
+# Import user's runtime.exs if exists
+[ -f "$CONFIG_DIR/runtime.exs" ] && export RELEASE_CONFIG_DIR="$CONFIG_DIR"
+
+# Socket/port configuration
+if [ -n "${UNRAID_PORT:-}" ]; then
+ export PHX_PORT="$UNRAID_PORT"
+else
+ export PHX_SOCKET="${UNRAID_SOCKET:-$SOCKET_PATH}"
+fi
+
+start() {
+ echo -n "Starting Unraid... "
+ [ -S "$SOCKET_PATH" ] && rm -f "$SOCKET_PATH"
+ "$RELEASE_BIN" daemon
+ echo "done"
+}
+
+stop() {
+ echo -n "Stopping Unraid... "
+ "$RELEASE_BIN" stop 2>/dev/null || true
+ [ -S "$SOCKET_PATH" ] && rm -f "$SOCKET_PATH"
+ echo "done"
+}
+
+restart() {
+ stop
+ sleep 2
+ start
+}
+
+status() {
+ "$RELEASE_BIN" pid >/dev/null 2>&1 && echo "Running" || echo "Stopped"
+}
+
+rollback() {
+ if [ -d "/usr/local/unraid.prev" ]; then
+ echo "Rolling back to previous version..."
+ stop
+ rm -rf /usr/local/unraid
+ mv /usr/local/unraid.prev /usr/local/unraid
+ start
+ echo "Rollback complete"
+ else
+ echo "No previous version available"
+ exit 1
+ fi
+}
+
+case "${1:-}" in
+ start) start ;;
+ stop) stop ;;
+ restart) restart ;;
+ status) status ;;
+ rollback) rollback ;;
+ *) echo "Usage: $0 {start|stop|restart|status|rollback}" ;;
+esac
diff --git a/plugin/source/dynamix.unraid.net/etc/rc.d/rc6.d/K30unraid-core b/plugin/source/dynamix.unraid.net/etc/rc.d/rc6.d/K30unraid-core
new file mode 100644
index 0000000000..82a004bbcd
--- /dev/null
+++ b/plugin/source/dynamix.unraid.net/etc/rc.d/rc6.d/K30unraid-core
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Stop Unraid Core on shutdown/reboot
+
+if [ -x /etc/rc.d/rc.unraid ]; then
+ echo "Stopping Unraid Core..."
+ /etc/rc.d/rc.unraid stop
+fi
diff --git a/plugin/source/dynamix.unraid.net/install/doinst.sh b/plugin/source/dynamix.unraid.net/install/doinst.sh
index e18f5f64eb..79b6e2292f 100644
--- a/plugin/source/dynamix.unraid.net/install/doinst.sh
+++ b/plugin/source/dynamix.unraid.net/install/doinst.sh
@@ -6,7 +6,7 @@ backup_file_if_exists() {
fi
}
-for f in etc/rc.d/rc6.d/K*unraid-api etc/rc.d/rc6.d/K*flash-backup; do
+for f in etc/rc.d/rc6.d/K*unraid-api etc/rc.d/rc6.d/K*unraid-core etc/rc.d/rc6.d/K*flash-backup; do
[ -e "$f" ] && chmod 755 "$f"
done
diff --git a/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/install/scripts/verify_install.sh b/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/install/scripts/verify_install.sh
index 0731bd976e..107d44aa87 100755
--- a/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/install/scripts/verify_install.sh
+++ b/plugin/source/dynamix.unraid.net/usr/local/share/dynamix.unraid.net/install/scripts/verify_install.sh
@@ -42,10 +42,12 @@ echo "Performing comprehensive installation verification..."
# Define critical files to check (POSIX-compliant, no arrays)
CRITICAL_FILES="/usr/local/bin/unraid-api
/etc/rc.d/rc.unraid-api
+/etc/rc.d/rc.unraid
/usr/local/emhttp/plugins/dynamix.my.servers/scripts/gitflash_log"
# Define critical directories to check (POSIX-compliant, no arrays)
CRITICAL_DIRS="/usr/local/unraid-api
+/usr/local/unraid
/var/log/unraid-api
/usr/local/emhttp/plugins/dynamix.my.servers
/usr/local/emhttp/plugins/dynamix.unraid.net
@@ -159,6 +161,14 @@ else
SHUTDOWN_ERRORS=$((SHUTDOWN_ERRORS + 1))
fi
+# Check for unraid-core shutdown script
+if [ -x "/etc/rc.d/rc6.d/K30unraid-core" ]; then
+ printf '✓ Shutdown script for unraid-core exists and is executable\n'
+else
+ printf '✗ Shutdown script for unraid-core missing or not executable\n'
+ SHUTDOWN_ERRORS=$((SHUTDOWN_ERRORS + 1))
+fi
+
# Check for rc0.d symlink or directory
if [ -L "/etc/rc.d/rc0.d" ]; then
printf '✓ rc0.d symlink exists\n'
@@ -206,4 +216,4 @@ else
echo "Please review the errors above and contact support if needed."
# We don't exit with error as this is just a verification script
exit 0
-fi
\ No newline at end of file
+fi
diff --git a/web/__test__/components/SsoButton.test.ts b/web/__test__/components/SsoButton.test.ts
index c88c622ae0..cf3f7cd7ac 100644
--- a/web/__test__/components/SsoButton.test.ts
+++ b/web/__test__/components/SsoButton.test.ts
@@ -253,14 +253,7 @@ describe('SsoButtons', () => {
const button = wrapper.find('button');
await button.trigger('click');
- // Should set state and provider in sessionStorage
- expect(sessionStorage.setItem).toHaveBeenCalledWith('sso_state', expect.any(String));
- expect(sessionStorage.setItem).toHaveBeenCalledWith('sso_provider', 'unraid-net');
-
- const generatedState = (sessionStorage.setItem as Mock).mock.calls[0][1];
- const redirectUri = `${mockLocation.origin}/graphql/api/auth/oidc/callback`;
- const expectedUrl = `/graphql/api/auth/oidc/authorize/unraid-net?state=${encodeURIComponent(generatedState)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
-
+ const expectedUrl = `/auth/sso/unraid-net`;
expect(mockLocation.href).toBe(expectedUrl);
});
@@ -349,95 +342,6 @@ describe('SsoButtons', () => {
expect(mockHistory.replaceState).toHaveBeenCalledWith({}, 'Mock Title', expectedUrl);
});
- it('redirects to OIDC callback endpoint when code and state are present', async () => {
- const mockProviders = [
- {
- id: 'unraid-net',
- name: 'Unraid.net',
- buttonText: 'Log In With Unraid.net',
- },
- ];
-
- mockUseQuery.mockReturnValue({
- result: { value: { publicOidcProviders: mockProviders } },
- refetch: vi.fn().mockResolvedValue({ data: { publicOidcProviders: mockProviders } }),
- });
-
- const mockCode = 'mock_auth_code';
- const mockState = 'mock_session_state_value';
-
- mockLocation.search = `?code=${mockCode}&state=${mockState}`;
- mockLocation.pathname = '/login';
-
- mount(SsoButtons, {
- global: {
- plugins: [createTestI18n()],
- stubs: {
- SsoProviderButton: SsoProviderButtonStub,
- Button: { template: '' },
- },
- },
- });
-
- await flushPromises();
-
- // Should redirect to the OIDC callback endpoint
- const expectedUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(mockCode)}&state=${encodeURIComponent(mockState)}`;
- expect(mockLocation.href).toBe(expectedUrl);
- });
-
- it('handles HTTPS with non-standard port correctly', async () => {
- const mockProviders = [
- {
- id: 'tsidp',
- name: 'Tailscale IDP',
- buttonText: 'Sign in with Tailscale',
- buttonIcon: null,
- buttonVariant: 'secondary',
- buttonStyle: null,
- },
- ];
-
- // Set up location with HTTPS and non-standard port
- mockLocation.protocol = 'https:';
- mockLocation.host = 'unraid.mytailnet.ts.net:1443';
- mockLocation.origin = 'https://unraid.mytailnet.ts.net:1443';
-
- mockUseQuery.mockReturnValue({
- result: { value: { publicOidcProviders: mockProviders } },
- refetch: vi.fn().mockResolvedValue({ data: { publicOidcProviders: mockProviders } }),
- });
-
- const wrapper = mount(SsoButtons, {
- global: {
- plugins: [createTestI18n()],
- stubs: {
- SsoProviderButton: SsoProviderButtonStub,
- Button: { template: '' },
- },
- },
- });
-
- await flushPromises();
- vi.runAllTimers();
- await flushPromises();
-
- const button = wrapper.find('button');
- await button.trigger('click');
-
- // Should include the correct redirect URI with HTTPS and port 1443
- const generatedState = (sessionStorage.setItem as Mock).mock.calls[0][1];
- const redirectUri = 'https://unraid.mytailnet.ts.net:1443/graphql/api/auth/oidc/callback';
- const expectedUrl = `/graphql/api/auth/oidc/authorize/tsidp?state=${encodeURIComponent(generatedState)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
-
- expect(mockLocation.href).toBe(expectedUrl);
-
- // Reset location mock for other tests
- mockLocation.protocol = 'http:';
- mockLocation.host = 'mock-origin.com';
- mockLocation.origin = 'http://mock-origin.com';
- });
-
it('handles multiple OIDC providers', async () => {
const mockProviders = [
{
diff --git a/web/src/components/sso/useSsoAuth.ts b/web/src/components/sso/useSsoAuth.ts
index 2b3d1f2573..573bfa45ab 100644
--- a/web/src/components/sso/useSsoAuth.ts
+++ b/web/src/components/sso/useSsoAuth.ts
@@ -35,19 +35,6 @@ export function useSsoAuth() {
form.requestSubmit();
};
- const getStateToken = (): string | null => {
- const state = sessionStorage.getItem('sso_state');
- return state ?? null;
- };
-
- const generateStateToken = (): string => {
- const array = new Uint8Array(32);
- window.crypto.getRandomValues(array);
- const state = Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
- sessionStorage.setItem('sso_state', state);
- return state;
- };
-
const disableFormOnSubmit = () => {
const fields = getInputFields();
if (fields?.form) {
@@ -63,19 +50,9 @@ export function useSsoAuth() {
};
const navigateToProvider = (providerId: string) => {
- // Generate state token for CSRF protection
- const state = generateStateToken();
-
- // Store provider ID separately since state must be alphanumeric only
- sessionStorage.setItem('sso_state', state);
- sessionStorage.setItem('sso_provider', providerId);
-
- // Build the redirect URI based on current window location
- const redirectUri = `${window.location.protocol}//${window.location.host}/graphql/api/auth/oidc/callback`;
-
- // Redirect to OIDC authorization endpoint with state token and redirect URI
- const authUrl = `/graphql/api/auth/oidc/authorize/${encodeURIComponent(providerId)}?state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
- window.location.href = authUrl;
+ currentState.value = 'loading';
+ error.value = null;
+ window.location.href = `/auth/sso/${encodeURIComponent(providerId)}`;
};
const handleOAuthCallback = async () => {
@@ -85,11 +62,8 @@ export function useSsoAuth() {
const hashToken = hashParams.get('token');
const hashError = hashParams.get('error');
- // Then check query parameters (for OAuth code/state from provider redirects)
+ // Then check query parameters (for error/token fallback)
const search = new URLSearchParams(window.location.search);
- const code = search.get('code') ?? '';
- const state = search.get('state') ?? '';
- const sessionState = getStateToken();
// Check for error in hash (preferred) or query params (fallback)
const errorParam = hashError || search.get('error') || '';
@@ -114,21 +88,9 @@ export function useSsoAuth() {
return;
}
- // Handle Unraid.net SSO callback (comes to /login with code and state)
- if (code && state && window.location.pathname === '/login') {
- currentState.value = 'loading';
-
- // Redirect to our OIDC callback endpoint to exchange the code
- const callbackUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
- window.location.href = callbackUrl;
+ if (window.location.pathname !== '/login') {
return;
}
-
- // Error if we have mismatched state
- if (code && state && state !== sessionState) {
- currentState.value = 'error';
- error.value = t('sso.useSsoAuth.invalidCallbackParameters');
- }
} catch (err) {
console.error('Error fetching token', err);
currentState.value = 'error';
From 537f78350cea9661443d88ffbed1c912f41f532e Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Thu, 15 Jan 2026 05:31:56 -0500
Subject: [PATCH 2/9] chore: update core_ release
---
plugin/plugins/dynamix.unraid.net.plg | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index da2b61b70c..b767b27988 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -10,9 +10,9 @@
-
-
-
+
+
+
From f73919a156e34175aab05ccadb402c2cfdd98f48 Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Thu, 15 Jan 2026 06:42:02 -0500
Subject: [PATCH 3/9] fix: sso auth
---
.../snapshots/rc.nginx.modified.snapshot | 11 ++++++++
.../modifications/patches/rc-nginx.patch | 28 +++++++++++++++++--
.../modifications/rc-nginx.modification.ts | 8 ++++++
plugin/plugins/dynamix.unraid.net.plg | 2 +-
.../dynamix.unraid.net/etc/rc.d/rc.unraid | 1 +
5 files changed, 46 insertions(+), 4 deletions(-)
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
index cdab15b5d8..4064118890 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
@@ -383,6 +383,17 @@ build_locations(){
include fastcgi_params;
}
#
+ # SSO endpoints (public)
+ location /auth/sso {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+ #
# Redirect to login page on failed authentication (401)
#
error_page 401 @401;
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
index 9eea949545..d615cd2d9a 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
@@ -44,7 +44,29 @@ Index: /etc/rc.d/rc.nginx
T=' '
if check && [[ $1 == lo ]]; then
if [[ $IPV4 == yes ]]; then
-@@ -400,11 +418,11 @@
+@@ -363,10 +381,21 @@
+ allow all;
+ try_files /login.php =404;
+ include fastcgi_params;
+ }
+ #
++ # SSO endpoints (public)
++ location /auth/sso {
++ allow all;
++ proxy_pass http://unix:/var/run/unraid-core.sock:;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++ #
+ # Redirect to login page on failed authentication (401)
+ #
+ error_page 401 @401;
+ location @401 {
+ return 302 $scheme://$http_host/login;
+@@ -400,11 +429,11 @@
# my servers proxy
#
location /graphql {
@@ -57,7 +79,7 @@ Index: /etc/rc.d/rc.nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
-@@ -566,11 +584,11 @@
+@@ -566,11 +595,11 @@
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
@@ -70,7 +92,7 @@ Index: /etc/rc.d/rc.nginx
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
-@@ -660,14 +678,14 @@
+@@ -660,14 +689,14 @@
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
index 1e21880152..c3f83c603f 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
@@ -73,6 +73,14 @@ check_remote_access(){
'proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;'
);
+ if (!newContent.includes('location /auth/sso')) {
+ newContent = newContent.replace(
+ '\t# Redirect to login page on failed authentication (401)\n',
+ // prettier-ignore
+ `\t# SSO endpoints (public)\n\tlocation /auth/sso {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-core.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\t#\n\t# Redirect to login page on failed authentication (401)\n`
+ );
+ }
+
newContent = newContent.replace(
'for NET in ${!NET_FQDN6[@]}; do',
'for NET in "${!NET_FQDN6[@]}"; do'
diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index b767b27988..dfc2f11285 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -10,7 +10,7 @@
-
+
diff --git a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
index d7c35b4abd..80f636e20c 100755
--- a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
+++ b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
@@ -29,6 +29,7 @@ export RUN_ERL_LOG="${RUN_ERL_LOG:-$LOG_PATH}"
export RELEASE_LOG_DIR="${RELEASE_LOG_DIR:-$(dirname "$LOG_PATH")}"
export RELEASE_NODE="${UNRAID_RELEASE_NODE:-unraid}"
export RELEASE_DISTRIBUTION="${UNRAID_RELEASE_DISTRIBUTION:-sname}"
+export RELEASE_MODE="${UNRAID_RELEASE_MODE:-interactive}"
# Import user's runtime.exs if exists
[ -f "$CONFIG_DIR/runtime.exs" ] && export RELEASE_CONFIG_DIR="$CONFIG_DIR"
From 9ddae2e3adc5b099572ebef5c42432efc3b3e1f6 Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Thu, 15 Jan 2026 08:25:41 -0500
Subject: [PATCH 4/9] fix: sso compat
---
.../modifications/patches/rc-nginx.patch | 23 ++++-
.../modifications/rc-nginx.modification.ts | 10 +-
web/__test__/components/SsoButton.test.ts | 98 ++++++++++++++++++-
web/src/components/sso/useSsoAuth.ts | 46 ++++++++-
web/src/helpers/create-apollo-client.ts | 5 +-
5 files changed, 174 insertions(+), 8 deletions(-)
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
index d615cd2d9a..acbd35f4b6 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
@@ -66,20 +66,35 @@ Index: /etc/rc.d/rc.nginx
error_page 401 @401;
location @401 {
return 302 $scheme://$http_host/login;
-@@ -400,11 +429,11 @@
+@@ -397,14 +426,26 @@
+ nchan_stub_status;
+ }
+ #
# my servers proxy
#
++ location /graphql/api {
++ allow all;
++ proxy_pass http://unix:/var/run/unraid-api.sock:;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
location /graphql {
allow all;
error_log /dev/null crit;
- proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;
-+ proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;
++ if ($http_upgrade = "websocket") {
++ rewrite ^/graphql$ /graphql/socket break;
++ }
++ proxy_pass http://unix:/var/run/unraid-core.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
-@@ -566,11 +595,11 @@
+@@ -566,11 +607,11 @@
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
@@ -92,7 +107,7 @@ Index: /etc/rc.d/rc.nginx
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
-@@ -660,14 +689,14 @@
+@@ -660,14 +701,14 @@
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
index c3f83c603f..28d396f890 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
@@ -70,7 +70,7 @@ check_remote_access(){
newContent = newContent.replace(
'proxy_pass http://unix:/var/run/unraid-api.sock:/graphql;',
- 'proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;'
+ 'if ($http_upgrade = "websocket") {\n\t rewrite ^/graphql$ /graphql/socket break;\n\t }\n\t proxy_pass http://unix:/var/run/unraid-core.sock:;'
);
if (!newContent.includes('location /auth/sso')) {
@@ -81,6 +81,14 @@ check_remote_access(){
);
}
+ if (!newContent.includes('location /graphql/api')) {
+ newContent = newContent.replace(
+ '\t# my servers proxy\n\t#\n\tlocation /graphql {',
+ // prettier-ignore
+ `\t# my servers proxy\n\t#\n\tlocation /graphql/api {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-api.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\tlocation /graphql {`
+ );
+ }
+
newContent = newContent.replace(
'for NET in ${!NET_FQDN6[@]}; do',
'for NET in "${!NET_FQDN6[@]}"; do'
diff --git a/web/__test__/components/SsoButton.test.ts b/web/__test__/components/SsoButton.test.ts
index cf3f7cd7ac..c88c622ae0 100644
--- a/web/__test__/components/SsoButton.test.ts
+++ b/web/__test__/components/SsoButton.test.ts
@@ -253,7 +253,14 @@ describe('SsoButtons', () => {
const button = wrapper.find('button');
await button.trigger('click');
- const expectedUrl = `/auth/sso/unraid-net`;
+ // Should set state and provider in sessionStorage
+ expect(sessionStorage.setItem).toHaveBeenCalledWith('sso_state', expect.any(String));
+ expect(sessionStorage.setItem).toHaveBeenCalledWith('sso_provider', 'unraid-net');
+
+ const generatedState = (sessionStorage.setItem as Mock).mock.calls[0][1];
+ const redirectUri = `${mockLocation.origin}/graphql/api/auth/oidc/callback`;
+ const expectedUrl = `/graphql/api/auth/oidc/authorize/unraid-net?state=${encodeURIComponent(generatedState)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
+
expect(mockLocation.href).toBe(expectedUrl);
});
@@ -342,6 +349,95 @@ describe('SsoButtons', () => {
expect(mockHistory.replaceState).toHaveBeenCalledWith({}, 'Mock Title', expectedUrl);
});
+ it('redirects to OIDC callback endpoint when code and state are present', async () => {
+ const mockProviders = [
+ {
+ id: 'unraid-net',
+ name: 'Unraid.net',
+ buttonText: 'Log In With Unraid.net',
+ },
+ ];
+
+ mockUseQuery.mockReturnValue({
+ result: { value: { publicOidcProviders: mockProviders } },
+ refetch: vi.fn().mockResolvedValue({ data: { publicOidcProviders: mockProviders } }),
+ });
+
+ const mockCode = 'mock_auth_code';
+ const mockState = 'mock_session_state_value';
+
+ mockLocation.search = `?code=${mockCode}&state=${mockState}`;
+ mockLocation.pathname = '/login';
+
+ mount(SsoButtons, {
+ global: {
+ plugins: [createTestI18n()],
+ stubs: {
+ SsoProviderButton: SsoProviderButtonStub,
+ Button: { template: '' },
+ },
+ },
+ });
+
+ await flushPromises();
+
+ // Should redirect to the OIDC callback endpoint
+ const expectedUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(mockCode)}&state=${encodeURIComponent(mockState)}`;
+ expect(mockLocation.href).toBe(expectedUrl);
+ });
+
+ it('handles HTTPS with non-standard port correctly', async () => {
+ const mockProviders = [
+ {
+ id: 'tsidp',
+ name: 'Tailscale IDP',
+ buttonText: 'Sign in with Tailscale',
+ buttonIcon: null,
+ buttonVariant: 'secondary',
+ buttonStyle: null,
+ },
+ ];
+
+ // Set up location with HTTPS and non-standard port
+ mockLocation.protocol = 'https:';
+ mockLocation.host = 'unraid.mytailnet.ts.net:1443';
+ mockLocation.origin = 'https://unraid.mytailnet.ts.net:1443';
+
+ mockUseQuery.mockReturnValue({
+ result: { value: { publicOidcProviders: mockProviders } },
+ refetch: vi.fn().mockResolvedValue({ data: { publicOidcProviders: mockProviders } }),
+ });
+
+ const wrapper = mount(SsoButtons, {
+ global: {
+ plugins: [createTestI18n()],
+ stubs: {
+ SsoProviderButton: SsoProviderButtonStub,
+ Button: { template: '' },
+ },
+ },
+ });
+
+ await flushPromises();
+ vi.runAllTimers();
+ await flushPromises();
+
+ const button = wrapper.find('button');
+ await button.trigger('click');
+
+ // Should include the correct redirect URI with HTTPS and port 1443
+ const generatedState = (sessionStorage.setItem as Mock).mock.calls[0][1];
+ const redirectUri = 'https://unraid.mytailnet.ts.net:1443/graphql/api/auth/oidc/callback';
+ const expectedUrl = `/graphql/api/auth/oidc/authorize/tsidp?state=${encodeURIComponent(generatedState)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
+
+ expect(mockLocation.href).toBe(expectedUrl);
+
+ // Reset location mock for other tests
+ mockLocation.protocol = 'http:';
+ mockLocation.host = 'mock-origin.com';
+ mockLocation.origin = 'http://mock-origin.com';
+ });
+
it('handles multiple OIDC providers', async () => {
const mockProviders = [
{
diff --git a/web/src/components/sso/useSsoAuth.ts b/web/src/components/sso/useSsoAuth.ts
index 573bfa45ab..1806b1fcd4 100644
--- a/web/src/components/sso/useSsoAuth.ts
+++ b/web/src/components/sso/useSsoAuth.ts
@@ -35,6 +35,19 @@ export function useSsoAuth() {
form.requestSubmit();
};
+ const getStateToken = (): string | null => {
+ const state = sessionStorage.getItem('sso_state');
+ return state ?? null;
+ };
+
+ const generateStateToken = (): string => {
+ const array = new Uint8Array(32);
+ window.crypto.getRandomValues(array);
+ const state = Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
+ sessionStorage.setItem('sso_state', state);
+ return state;
+ };
+
const disableFormOnSubmit = () => {
const fields = getInputFields();
if (fields?.form) {
@@ -52,7 +65,19 @@ export function useSsoAuth() {
const navigateToProvider = (providerId: string) => {
currentState.value = 'loading';
error.value = null;
- window.location.href = `/auth/sso/${encodeURIComponent(providerId)}`;
+ // Generate state token for CSRF protection
+ const state = generateStateToken();
+
+ // Store provider ID separately since state must be alphanumeric only
+ sessionStorage.setItem('sso_state', state);
+ sessionStorage.setItem('sso_provider', providerId);
+
+ // Build the redirect URI based on current window location
+ const redirectUri = `${window.location.protocol}//${window.location.host}/graphql/api/auth/oidc/callback`;
+
+ // Redirect to OIDC authorization endpoint with state token and redirect URI
+ const authUrl = `/graphql/api/auth/oidc/authorize/${encodeURIComponent(providerId)}?state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
+ window.location.href = authUrl;
};
const handleOAuthCallback = async () => {
@@ -64,6 +89,9 @@ export function useSsoAuth() {
// Then check query parameters (for error/token fallback)
const search = new URLSearchParams(window.location.search);
+ const code = search.get('code') ?? '';
+ const state = search.get('state') ?? '';
+ const sessionState = getStateToken();
// Check for error in hash (preferred) or query params (fallback)
const errorParam = hashError || search.get('error') || '';
@@ -88,6 +116,22 @@ export function useSsoAuth() {
return;
}
+ // Handle Unraid.net SSO callback (comes to /login with code and state)
+ if (code && state && window.location.pathname === '/login') {
+ currentState.value = 'loading';
+
+ // Redirect to our OIDC callback endpoint to exchange the code
+ const callbackUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
+ window.location.href = callbackUrl;
+ return;
+ }
+
+ // Error if we have mismatched state
+ if (code && state && state !== sessionState) {
+ currentState.value = 'error';
+ error.value = t('sso.useSsoAuth.invalidCallbackParameters');
+ }
+
if (window.location.pathname !== '/login') {
return;
}
diff --git a/web/src/helpers/create-apollo-client.ts b/web/src/helpers/create-apollo-client.ts
index f3e0c0adaf..c45c5491a9 100644
--- a/web/src/helpers/create-apollo-client.ts
+++ b/web/src/helpers/create-apollo-client.ts
@@ -43,8 +43,11 @@ const wsEndpoint = new URL(httpEndpoint);
wsEndpoint.protocol = wsEndpoint.protocol === 'https:' ? 'wss:' : 'ws:';
const DEV_MODE = (globalThis as unknown as { __DEV__: boolean }).__DEV__ ?? false;
+const csrfToken = globalThis.csrf_token ?? '0000000000000000';
+wsEndpoint.searchParams.set('_csrf_token', csrfToken);
+
const headers = {
- 'x-csrf-token': globalThis.csrf_token ?? '0000000000000000',
+ 'x-csrf-token': csrfToken,
};
const httpLink = createHttpLink({
From f83aa5cf1306fda1d4a47fd829ea1dab68f4b38e Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Thu, 15 Jan 2026 08:27:50 -0500
Subject: [PATCH 5/9] chore: update unraid_core
---
plugin/plugins/dynamix.unraid.net.plg | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index dfc2f11285..6e795a94b3 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -10,7 +10,7 @@
-
+
From 9557998bb3561f670e73066dc215e91fd25554ea Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Thu, 15 Jan 2026 08:35:58 -0500
Subject: [PATCH 6/9] test: update rc.nginx snapshot
---
.../__test__/snapshots/rc.nginx.modified.snapshot | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
index 4064118890..3b24e4777c 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
@@ -428,10 +428,22 @@ build_locations(){
#
# my servers proxy
#
+ location /graphql/api {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-api.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
location /graphql {
allow all;
error_log /dev/null crit;
- proxy_pass http://unix:/var/run/unraid-core.sock:/graphql;
+ if ($http_upgrade = "websocket") {
+ rewrite ^/graphql$ /graphql/socket break;
+ }
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
From 84df97d174eb94840d260edca0a0f53c516b2755 Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Tue, 20 Jan 2026 14:30:18 -0500
Subject: [PATCH 7/9] chore: address pr comments
---
.../.login.php.modified.snapshot.php | 48 +++++++----
.../snapshots/rc.nginx.modified.snapshot | 9 ++
.../modifications/rc-nginx.modification.ts | 10 ++-
.../modifications/sso.modification.ts | 55 +++++++++----
plugin/plugins/dynamix.unraid.net.plg | 12 +++
.../dynamix.unraid.net/etc/rc.d/rc.unraid | 28 ++++++-
web/__test__/components/SsoButton.test.ts | 82 +++++++++++++++++++
7 files changed, 205 insertions(+), 39 deletions(-)
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
index b59772fc12..ef0bc9658a 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
@@ -17,30 +17,48 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;
}
- $safePassword = escapeshellarg($password);
-
- $output = array();
- exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword 2>&1", $output, $code);
+ $payload = json_encode(["token" => $password]);
+ $response = false;
+ $code = 0;
+
+ if (function_exists("curl_init")) {
+ $ch = curl_init("http://127.0.0.1/auth/sso/validate");
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+ $response = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+ curl_close($ch);
+ } else {
+ $context = stream_context_create([
+ "http" => [
+ "method" => "POST",
+ "header" => "Content-Type: application/json\r\n",
+ "content" => $payload,
+ "timeout" => 5,
+ ],
+ ]);
+ $response = @file_get_contents("http://127.0.0.1/auth/sso/validate", false, $context);
+ if (isset($http_response_header[0])) {
+ $code = (int) preg_replace('/^HTTP\\/[0-9.]+\\s+(\\d+).*/', '$1', $http_response_header[0]);
+ }
+ }
my_logger("SSO Login Attempt Code: $code");
- my_logger("SSO Login Attempt Response: " . print_r($output, true));
+ my_logger("SSO Login Attempt Response: " . print_r($response, true));
- if ($code !== 0) {
+ if ($code !== 200) {
return false;
}
- if (empty($output)) {
+ if (empty($response)) {
return false;
}
try {
- // Split on first { and take everything after it
- $jsonParts = explode('{', $output[0], 2);
- if (count($jsonParts) < 2) {
- my_logger("SSO Login Attempt Failed: No JSON found in response");
- return false;
- }
- $response = json_decode('{' . $jsonParts[1], true);
- if (isset($response['valid']) && $response['valid'] === true) {
+ $decoded = json_decode($response, true);
+ if (isset($decoded['valid']) && $decoded['valid'] === true) {
return true;
}
} catch (Exception $e) {
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
index 3b24e4777c..b1f68cd327 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
@@ -428,6 +428,15 @@ build_locations(){
#
# my servers proxy
#
+ location /graphql/api/auth/oidc {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
location /graphql/api {
allow all;
proxy_pass http://unix:/var/run/unraid-api.sock:;
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
index 28d396f890..148ce78fa0 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/rc-nginx.modification.ts
@@ -7,9 +7,11 @@ import {
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
/**
- * Patch rc.nginx on < Unraid 7.2.0 to read the updated connect & api config files
+ * Patch rc.nginx to read the updated connect & api config files.
*
- * Backport of https://github.com/unraid/webgui/pull/2269
+ * Backport of https://github.com/unraid/webgui/pull/2269. This modification
+ * runs on all versions but uses idempotent guards to avoid double-injection
+ * when the base OS already includes the changes.
*/
export default class RcNginxModification extends FileModification {
public filePath: string = '/etc/rc.d/rc.nginx' as const;
@@ -81,11 +83,11 @@ check_remote_access(){
);
}
- if (!newContent.includes('location /graphql/api')) {
+ if (!newContent.includes('location /graphql/api/auth/oidc')) {
newContent = newContent.replace(
'\t# my servers proxy\n\t#\n\tlocation /graphql {',
// prettier-ignore
- `\t# my servers proxy\n\t#\n\tlocation /graphql/api {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-api.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\tlocation /graphql {`
+ `\t# my servers proxy\n\t#\n\tlocation /graphql/api/auth/oidc {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-core.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\tlocation /graphql/api {\n\t allow all;\n\t proxy_pass http://unix:/var/run/unraid-api.sock:;\n\t proxy_http_version 1.1;\n\t proxy_set_header Host $host;\n\t proxy_set_header X-Real-IP $remote_addr;\n\t proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t proxy_set_header X-Forwarded-Proto $scheme;\n\t}\n\tlocation /graphql {`
);
}
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts b/api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts
index aef9b7dce6..9cda627165 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts
@@ -9,6 +9,11 @@ export default class SSOFileModification extends FileModification {
id: string = 'sso';
public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
+ protected async getPregeneratedPatch(): Promise {
+ // Prefer the dynamic patch to avoid stale pregenerated SSO patches.
+ return null;
+ }
+
protected async generatePatch(overridePath?: string): Promise {
// Define the new PHP function to insert
/* eslint-disable no-useless-escape */
@@ -29,30 +34,48 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
my_logger("SSO Login Attempt Failed: Invalid token format");
return false;
}
- $safePassword = escapeshellarg($password);
-
- $output = array();
- exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword 2>&1", $output, $code);
+ $payload = json_encode(["token" => $password]);
+ $response = false;
+ $code = 0;
+
+ if (function_exists("curl_init")) {
+ $ch = curl_init("http://127.0.0.1/auth/sso/validate");
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+ $response = curl_exec($ch);
+ $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
+ curl_close($ch);
+ } else {
+ $context = stream_context_create([
+ "http" => [
+ "method" => "POST",
+ "header" => "Content-Type: application/json\r\n",
+ "content" => $payload,
+ "timeout" => 5,
+ ],
+ ]);
+ $response = @file_get_contents("http://127.0.0.1/auth/sso/validate", false, $context);
+ if (isset($http_response_header[0])) {
+ $code = (int) preg_replace('/^HTTP\\/[0-9.]+\\s+(\\d+).*/', '$1', $http_response_header[0]);
+ }
+ }
my_logger("SSO Login Attempt Code: $code");
- my_logger("SSO Login Attempt Response: " . print_r($output, true));
+ my_logger("SSO Login Attempt Response: " . print_r($response, true));
- if ($code !== 0) {
+ if ($code !== 200) {
return false;
}
- if (empty($output)) {
+ if (empty($response)) {
return false;
}
try {
- // Split on first { and take everything after it
- $jsonParts = explode('{', $output[0], 2);
- if (count($jsonParts) < 2) {
- my_logger("SSO Login Attempt Failed: No JSON found in response");
- return false;
- }
- $response = json_decode('{' . $jsonParts[1], true);
- if (isset($response['valid']) && $response['valid'] === true) {
+ $decoded = json_decode($response, true);
+ if (isset($decoded['valid']) && $decoded['valid'] === true) {
return true;
}
} catch (Exception $e) {
@@ -89,7 +112,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
}
async shouldApply(): Promise {
- const superShouldApply = await super.shouldApply();
+ const superShouldApply = await super.shouldApply({ checkOsVersion: false });
if (!superShouldApply.shouldApply) {
return superShouldApply;
}
diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index 6e795a94b3..e6fb8637d5 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -463,6 +463,9 @@ compare_versions() {
return $result
}
+# Track whether we updated the API package during this run.
+API_PKG_INSTALLED=0
+
# Check if API is already installed and get its version
CURRENT_API_VERSION=""
if [ -f "/usr/local/share/dynamix.unraid.net/config/vendor_archive.json" ] && command -v jq >/dev/null 2>&1; then
@@ -542,6 +545,7 @@ if [ "$SKIP_API_INSTALL" = false ]; then
echo "⚠️ Package installation failed"
exit 1
fi
+ API_PKG_INSTALLED=1
if [[ -n "$TAG" && "$TAG" != "" ]]; then
printf -v sedcmd 's@^\*\*Unraid Connect\*\*@**Unraid Connect (%s)**@' "$TAG"
@@ -572,6 +576,10 @@ if [ -f "$CORE_PKG_FILE" ]; then
upgradepkg --install-new --reinstall "${CORE_PKG_FILE}"
if [ $? -ne 0 ]; then
echo "⚠️ Core package installation failed"
+ if [ "$API_PKG_INSTALLED" -eq 1 ]; then
+ echo "⚠️ Unraid API package was installed; leaving it in place for troubleshooting."
+ echo " Re-run install or uninstall via Plugins > Installed Plugins to rollback."
+ fi
exit 1
fi
@@ -580,6 +588,10 @@ if [ -f "$CORE_PKG_FILE" ]; then
fi
else
echo "⚠️ Core package file not found: $CORE_PKG_FILE"
+ if [ "$API_PKG_INSTALLED" -eq 1 ]; then
+ echo "⚠️ Unraid API package was installed; leaving it in place for troubleshooting."
+ echo " Re-run install or uninstall via Plugins > Installed Plugins to rollback."
+ fi
exit 1
fi
diff --git a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
index 80f636e20c..49c881a867 100755
--- a/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
+++ b/plugin/source/dynamix.unraid.net/etc/rc.d/rc.unraid
@@ -66,16 +66,36 @@ status() {
}
rollback() {
- if [ -d "/usr/local/unraid.prev" ]; then
+ local current_dir="/usr/local/unraid"
+ local previous_dir="/usr/local/unraid.prev"
+ local temp_dir="/usr/local/unraid.tmp"
+
+ if [ -d "$previous_dir" ]; then
echo "Rolling back to previous version..."
stop
- rm -rf /usr/local/unraid
- mv /usr/local/unraid.prev /usr/local/unraid
+ if [ -e "$temp_dir" ]; then
+ echo "Rollback failed: temp backup already exists at $temp_dir"
+ return 1
+ fi
+ if ! mv "$current_dir" "$temp_dir"; then
+ echo "Rollback failed: unable to move current install to temp backup"
+ return 1
+ fi
+ if ! mv "$previous_dir" "$current_dir"; then
+ echo "Rollback failed: unable to restore previous version"
+ if [ -d "$temp_dir" ]; then
+ mv "$temp_dir" "$current_dir" || echo "Rollback recovery failed: unable to restore current install"
+ fi
+ return 1
+ fi
+ if ! rm -rf "$temp_dir"; then
+ echo "Rollback warning: unable to remove temp backup at $temp_dir"
+ fi
start
echo "Rollback complete"
else
echo "No previous version available"
- exit 1
+ return 1
fi
}
diff --git a/web/__test__/components/SsoButton.test.ts b/web/__test__/components/SsoButton.test.ts
index c88c622ae0..92aacb2d89 100644
--- a/web/__test__/components/SsoButton.test.ts
+++ b/web/__test__/components/SsoButton.test.ts
@@ -386,6 +386,88 @@ describe('SsoButtons', () => {
expect(mockLocation.href).toBe(expectedUrl);
});
+ it('shows an error when code/state do not match stored state', async () => {
+ const mockProviders = [
+ {
+ id: 'unraid-net',
+ name: 'Unraid.net',
+ buttonText: 'Log In With Unraid.net',
+ },
+ ];
+
+ mockUseQuery.mockReturnValue({
+ result: { value: { publicOidcProviders: mockProviders } },
+ refetch: vi.fn().mockResolvedValue({ data: { publicOidcProviders: mockProviders } }),
+ });
+
+ (sessionStorage.getItem as Mock).mockReturnValue('expected-state');
+
+ mockLocation.search = '?code=mock_code&state=unexpected_state';
+ mockLocation.pathname = '/not-login';
+
+ const wrapper = mount(SsoButtons, {
+ global: {
+ plugins: [createTestI18n()],
+ stubs: {
+ SsoProviderButton: SsoProviderButtonStub,
+ Button: { template: '' },
+ },
+ },
+ });
+
+ await flushPromises();
+
+ const errorElement = wrapper.find('p.text-red-500');
+ expect(errorElement.exists()).toBe(true);
+ expect(errorElement.text()).toBe('Invalid callback parameters');
+ });
+
+ it('handles unexpected callback errors', async () => {
+ const mockProviders = [
+ {
+ id: 'unraid-net',
+ name: 'Unraid.net',
+ buttonText: 'Log In With Unraid.net',
+ },
+ ];
+
+ mockUseQuery.mockReturnValue({
+ result: { value: { publicOidcProviders: mockProviders } },
+ refetch: vi.fn().mockResolvedValue({ data: { publicOidcProviders: mockProviders } }),
+ });
+
+ const originalURLSearchParams = globalThis.URLSearchParams;
+ vi.stubGlobal(
+ 'URLSearchParams',
+ vi.fn(() => {
+ throw new Error('boom');
+ })
+ );
+
+ mockLocation.search = '';
+ mockLocation.hash = '';
+ mockLocation.pathname = '/login';
+
+ const wrapper = mount(SsoButtons, {
+ global: {
+ plugins: [createTestI18n()],
+ stubs: {
+ SsoProviderButton: SsoProviderButtonStub,
+ Button: { template: '' },
+ },
+ },
+ });
+
+ await flushPromises();
+
+ const errorElement = wrapper.find('p.text-red-500');
+ expect(errorElement.exists()).toBe(true);
+ expect(errorElement.text()).toBe('Error fetching token');
+ expect(mockForm.style.display).toBe('block');
+
+ vi.stubGlobal('URLSearchParams', originalURLSearchParams);
+ });
+
it('handles HTTPS with non-standard port correctly', async () => {
const mockProviders = [
{
From d77b2b7e21317f544e7de2351a424051545b7cac Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Tue, 20 Jan 2026 15:13:23 -0500
Subject: [PATCH 8/9] chore: bump core package to 2026.01.20.1
---
.../.login.php.modified.snapshot.php | 5 +-
.../snapshots/rc.nginx.modified.snapshot | 9 ----
.../modifications/patches/rc-nginx.patch | 15 ++++--
.../modifications/patches/sso.patch | 53 +++++++++++++------
plugin/plugins/dynamix.unraid.net.plg | 6 +--
5 files changed, 54 insertions(+), 34 deletions(-)
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
index ef0bc9658a..b5d6c0e59b 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php
@@ -35,14 +35,15 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
$context = stream_context_create([
"http" => [
"method" => "POST",
- "header" => "Content-Type: application/json\r\n",
+ "header" => "Content-Type: application/json
+",
"content" => $payload,
"timeout" => 5,
],
]);
$response = @file_get_contents("http://127.0.0.1/auth/sso/validate", false, $context);
if (isset($http_response_header[0])) {
- $code = (int) preg_replace('/^HTTP\\/[0-9.]+\\s+(\\d+).*/', '$1', $http_response_header[0]);
+ $code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', $http_response_header[0]);
}
}
my_logger("SSO Login Attempt Code: $code");
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
index b1f68cd327..3b24e4777c 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
@@ -428,15 +428,6 @@ build_locations(){
#
# my servers proxy
#
- location /graphql/api/auth/oidc {
- allow all;
- proxy_pass http://unix:/var/run/unraid-core.sock:;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
location /graphql/api {
allow all;
proxy_pass http://unix:/var/run/unraid-api.sock:;
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
index acbd35f4b6..00bcf62618 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/rc-nginx.patch
@@ -66,12 +66,21 @@ Index: /etc/rc.d/rc.nginx
error_page 401 @401;
location @401 {
return 302 $scheme://$http_host/login;
-@@ -397,14 +426,26 @@
+@@ -397,14 +426,35 @@
nchan_stub_status;
}
#
# my servers proxy
#
++ location /graphql/api/auth/oidc {
++ allow all;
++ proxy_pass http://unix:/var/run/unraid-core.sock:;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
+ location /graphql/api {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-api.sock:;
@@ -94,7 +103,7 @@ Index: /etc/rc.d/rc.nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
-@@ -566,11 +607,11 @@
+@@ -566,11 +616,11 @@
# extract common name from cert
CERTNAME=$(openssl x509 -noout -subject -nameopt multiline -in $CERTPATH | sed -n 's/ *commonName *= //p')
# define CSP frame-ancestors for cert
@@ -107,7 +116,7 @@ Index: /etc/rc.d/rc.nginx
WANIP6=$(curl https://wanip6.unraid.net/ 2>/dev/null)
fi
if [[ $CERTNAME == *\.myunraid\.net ]]; then
-@@ -660,14 +701,14 @@
+@@ -660,14 +710,14 @@
echo "NGINX_WANFQDN=\"$WANFQDN\"" >>$INI
echo "NGINX_WANFQDN6=\"$WANFQDN6\"" >>$INI
# defined if ts_bundle.pem present:
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch b/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch
index f9fce692fd..78a2d12c52 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch
@@ -2,7 +2,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
===================================================================
--- /usr/local/emhttp/plugins/dynamix/include/.login.php original
+++ /usr/local/emhttp/plugins/dynamix/include/.login.php modified
-@@ -1,6 +1,57 @@
+@@ -1,6 +1,76 @@
$password]);
++ $response = false;
++ $code = 0;
+
-+ $output = array();
-+ exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword 2>&1", $output, $code);
++ if (function_exists("curl_init")) {
++ $ch = curl_init("http://127.0.0.1/auth/sso/validate");
++ curl_setopt($ch, CURLOPT_POST, true);
++ curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
++ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
++ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
++ curl_setopt($ch, CURLOPT_TIMEOUT, 5);
++ $response = curl_exec($ch);
++ $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
++ curl_close($ch);
++ } else {
++ $context = stream_context_create([
++ "http" => [
++ "method" => "POST",
++ "header" => "Content-Type: application/json
++",
++ "content" => $payload,
++ "timeout" => 5,
++ ],
++ ]);
++ $response = @file_get_contents("http://127.0.0.1/auth/sso/validate", false, $context);
++ if (isset($http_response_header[0])) {
++ $code = (int) preg_replace('/^HTTP\/[0-9.]+\s+(\d+).*/', '', $http_response_header[0]);
++ }
++ }
+ my_logger("SSO Login Attempt Code: $code");
-+ my_logger("SSO Login Attempt Response: " . print_r($output, true));
++ my_logger("SSO Login Attempt Response: " . print_r($response, true));
+
-+ if ($code !== 0) {
++ if ($code !== 200) {
+ return false;
+ }
+
-+ if (empty($output)) {
++ if (empty($response)) {
+ return false;
+ }
+
+ try {
-+ // Split on first { and take everything after it
-+ $jsonParts = explode('{', $output[0], 2);
-+ if (count($jsonParts) < 2) {
-+ my_logger("SSO Login Attempt Failed: No JSON found in response");
-+ return false;
-+ }
-+ $response = json_decode('{' . $jsonParts[1], true);
-+ if (isset($response['valid']) && $response['valid'] === true) {
++ $decoded = json_decode($response, true);
++ if (isset($decoded['valid']) && $decoded['valid'] === true) {
+ return true;
+ }
+ } catch (Exception $e) {
@@ -60,7 +79,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
// Only start a session to check if they have a cookie that looks like our session
$server_name = strtok($_SERVER['HTTP_HOST'], ":");
if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
-@@ -128,11 +179,11 @@
+@@ -128,11 +198,11 @@
}
throw new Exception(_('Too many invalid login attempts'));
}
@@ -73,7 +92,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/.login.php
// Successful login, start session
@unlink($failFile);
-@@ -434,10 +485,11 @@
+@@ -434,10 +504,11 @@
= $error ?>
diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg
index e6fb8637d5..5924f99743 100755
--- a/plugin/plugins/dynamix.unraid.net.plg
+++ b/plugin/plugins/dynamix.unraid.net.plg
@@ -10,9 +10,9 @@
-
-
-
+
+
+
From 8fdb128e2328d7eddae6b634315003f81fc66a8b Mon Sep 17 00:00:00 2001
From: Pujit Mehrotra
Date: Tue, 20 Jan 2026 15:34:42 -0500
Subject: [PATCH 9/9] test: update file mod snapshots
---
.../__test__/snapshots/rc.nginx.modified.snapshot | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
index 3b24e4777c..b1f68cd327 100644
--- a/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
+++ b/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/rc.nginx.modified.snapshot
@@ -428,6 +428,15 @@ build_locations(){
#
# my servers proxy
#
+ location /graphql/api/auth/oidc {
+ allow all;
+ proxy_pass http://unix:/var/run/unraid-core.sock:;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
location /graphql/api {
allow all;
proxy_pass http://unix:/var/run/unraid-api.sock:;