feat(auth): add silent OIDC sign-in (#799)

This commit is contained in:
TomyJan
2026-05-21 02:15:08 +08:00
committed by GitHub
parent d3fa8c4420
commit 5971bcfe88
5 changed files with 82 additions and 6 deletions
+29 -2
View File
@@ -36,6 +36,10 @@ import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronSe
import { ElectronLoginForm } from "@/auth/ElectronLoginForm";
import { Checkbox } from "@/components/checkbox";
import i18n from "@/i18n/i18n";
import {
removeSilentSigninFromSearch,
shouldTriggerSilentSignin,
} from "./silent-signin";
const LANGUAGES = [
{ code: "en", label: "English" },
@@ -234,6 +238,8 @@ export function Auth({ onLogin }: AuthProps) {
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
const [passwordResetAllowed, setPasswordResetAllowed] = useState(true);
const [oidcConfigured, setOidcConfigured] = useState(false);
const [oidcConfigLoaded, setOidcConfigLoaded] = useState(false);
const silentSigninHandledRef = useRef(false);
const [firstUser, setFirstUser] = useState(false);
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
const [dbHealthChecking, setDbHealthChecking] = useState(true);
@@ -262,7 +268,8 @@ export function Auth({ onLogin }: AuthProps) {
.catch(() => setPasswordResetAllowed(false));
getOIDCConfig()
.then((res) => setOidcConfigured(!!res))
.catch(() => setOidcConfigured(false));
.catch(() => setOidcConfigured(false))
.finally(() => setOidcConfigLoaded(true));
}, []);
useEffect(() => {
@@ -643,7 +650,7 @@ export function Auth({ onLogin }: AuthProps) {
}
}
async function handleOIDCLogin() {
const handleOIDCLogin = useCallback(async () => {
setOidcLoading(true);
try {
if (isElectron()) {
@@ -679,8 +686,28 @@ export function Auth({ onLogin }: AuthProps) {
);
setOidcLoading(false);
}
}, [rememberMe, t]);
useEffect(() => {
if (!oidcConfigLoaded || silentSigninHandledRef.current) return;
if (!shouldTriggerSilentSignin(window.location.search)) return;
const nextSearch = removeSilentSigninFromSearch(window.location.search);
window.history.replaceState(
{},
document.title,
`${window.location.pathname}${nextSearch}${window.location.hash}`,
);
silentSigninHandledRef.current = true;
if (oidcConfigured && !isElectron()) {
handleOIDCLogin();
return;
}
toast.info(t("errors.silentSigninOidcUnavailable"));
}, [handleOIDCLogin, oidcConfigLoaded, oidcConfigured, t]);
// Electron server config / webview auth success screens
if (isElectron() && !isInElectronWebView()) {
if (showServerConfig === null)
+31 -2
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/button.tsx";
import { Input } from "@/components/input.tsx";
import { PasswordInput } from "@/components/password-input.tsx";
@@ -31,6 +31,10 @@ import {
} from "@/main-axios";
import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx";
import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx";
import {
removeSilentSigninFromSearch,
shouldTriggerSilentSignin,
} from "./silent-signin";
function isMissingServerConfigError(error: unknown): boolean {
if (!(error instanceof Error)) {
@@ -123,6 +127,8 @@ export function Auth({
const [registrationAllowed, setRegistrationAllowed] = useState(true);
const [passwordLoginAllowed, setPasswordLoginAllowed] = useState(true);
const [oidcConfigured, setOidcConfigured] = useState(false);
const [oidcConfigLoaded, setOidcConfigLoaded] = useState(false);
const silentSigninHandledRef = useRef(false);
const [resetStep, setResetStep] = useState<
"initiate" | "verify" | "newPassword"
@@ -248,6 +254,9 @@ export function Auth({
} else {
setOidcConfigured(false);
}
})
.finally(() => {
setOidcConfigLoaded(true);
});
}, []);
@@ -625,7 +634,7 @@ export function Auth({
}
}
async function handleOIDCLogin() {
const handleOIDCLogin = useCallback(async () => {
setOidcLoading(true);
try {
const authResponse = await getOIDCAuthorizeUrl(rememberMe);
@@ -648,8 +657,28 @@ export function Auth({
toast.error(errorMessage);
setOidcLoading(false);
}
}, [rememberMe, t]);
useEffect(() => {
if (!oidcConfigLoaded || silentSigninHandledRef.current) return;
if (!shouldTriggerSilentSignin(window.location.search)) return;
const nextSearch = removeSilentSigninFromSearch(window.location.search);
window.history.replaceState(
{},
document.title,
`${window.location.pathname}${nextSearch}${window.location.hash}`,
);
silentSigninHandledRef.current = true;
if (oidcConfigured && !isElectron()) {
handleOIDCLogin();
return;
}
toast.info(t("errors.silentSigninOidcUnavailable"));
}, [handleOIDCLogin, oidcConfigLoaded, oidcConfigured, t]);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const success = urlParams.get("success");
+18
View File
@@ -0,0 +1,18 @@
export function shouldTriggerSilentSignin(search: string) {
const params = new URLSearchParams(search);
for (const [key, rawValue] of params) {
if (key.toLowerCase() !== "silentsignin") continue;
const value = rawValue;
return value === "" || value === "true" || value === "1";
}
return false;
}
export function removeSilentSigninFromSearch(search: string) {
const params = new URLSearchParams(search);
for (const key of Array.from(params.keys())) {
if (key.toLowerCase() === "silentsignin") params.delete(key);
}
const nextSearch = params.toString();
return nextSearch ? `?${nextSearch}` : "";
}
+1
View File
@@ -1226,6 +1226,7 @@
"failedCompleteReset": "Failed to complete password reset",
"invalidTotpCode": "Invalid TOTP code",
"failedOidcLogin": "Failed to start OIDC login",
"silentSigninOidcUnavailable": "Silent sign-in was requested, but OIDC login is not available.",
"failedUserInfo": "Failed to get user info after login",
"oidcAuthFailed": "OIDC authentication failed",
"invalidAuthUrl": "Invalid authorization URL received from backend",
+1
View File
@@ -2336,6 +2336,7 @@
"failedCompleteReset": "密码重置失败",
"invalidTotpCode": "无效的 TOTP 代码",
"failedOidcLogin": "OIDC 登录启动失败",
"silentSigninOidcUnavailable": "已请求静默登录,但当前未启用 OIDC 登录。",
"failedUserInfo": "登录后无法获取用户信息",
"oidcAuthFailed": "OIDC 身份验证失败",
"noTokenReceived": "登录未收到令牌",