feat(enterprise): add SAML 2.0 SSO with SP-initiated login

Implements SAML SSO using @node-saml/node-saml, gated behind
SAML_ENABLED env var and the saml_sso enterprise license feature.

- SAML env vars (entity ID, callback URL, IdP SSO URL, IdP cert,
  auto-create/auto-link users, default role, provider name,
  username/email attribute mapping) with validation in superRefine
- SAML plugin with three routes: metadata (GET), login (GET),
  and ACS callback (POST with form-urlencoded content type parser)
- Callback uses the shared external-auth resolver for user
  resolution (same pattern as OIDC: match/link/create/deny)
- Auth config endpoint exposes samlEnabled and samlProviderName
- Session loginMethod detection updated for SAML auth provider
- Frontend login page shows SAML SSO button when enabled
- i18n strings for SAML error messages across all 21 locales
This commit is contained in:
SnapOtter
2026-06-13 22:27:56 +08:00
parent 6920035f5a
commit 54132d1833
27 changed files with 383 additions and 13 deletions
+22 -1
View File
@@ -30,6 +30,7 @@ import {
ensureDefaultAdmin, ensureDefaultAdmin,
} from "./plugins/auth.js"; } from "./plugins/auth.js";
import { oidcRoutes } from "./plugins/oidc.js"; import { oidcRoutes } from "./plugins/oidc.js";
import { registerSaml } from "./plugins/saml.js";
import { registerStatic } from "./plugins/static.js"; import { registerStatic } from "./plugins/static.js";
import { registerUpload } from "./plugins/upload.js"; import { registerUpload } from "./plugins/upload.js";
import { adminOpsRoutes } from "./routes/admin-ops.js"; import { adminOpsRoutes } from "./routes/admin-ops.js";
@@ -39,6 +40,7 @@ import { auditLogRoutes } from "./routes/audit-log.js";
import { registerBatchRoutes } from "./routes/batch.js"; import { registerBatchRoutes } from "./routes/batch.js";
import { configRoutes } from "./routes/config.js"; import { configRoutes } from "./routes/config.js";
import { docsRoutes } from "./routes/docs.js"; import { docsRoutes } from "./routes/docs.js";
import { registerEnterpriseRoutes } from "./routes/enterprise/index.js";
import { registerFeatureRoutes } from "./routes/features.js"; import { registerFeatureRoutes } from "./routes/features.js";
import { registerFetchUrlsRoute } from "./routes/fetch-urls.js"; import { registerFetchUrlsRoute } from "./routes/fetch-urls.js";
import { fileRoutes } from "./routes/files.js"; import { fileRoutes } from "./routes/files.js";
@@ -50,7 +52,6 @@ import { settingsRoutes } from "./routes/settings.js";
import { teamsRoutes } from "./routes/teams.js"; import { teamsRoutes } from "./routes/teams.js";
import { registerToolRoutes } from "./routes/tools/index.js"; import { registerToolRoutes } from "./routes/tools/index.js";
import { userFileRoutes } from "./routes/user-files.js"; import { userFileRoutes } from "./routes/user-files.js";
import { registerEnterpriseRoutes } from "./routes/enterprise/index.js";
// Run before anything else // Run before anything else
try { try {
@@ -302,6 +303,9 @@ await authRoutes(app);
// OIDC routes // OIDC routes
await oidcRoutes(app); await oidcRoutes(app);
// SAML routes
await registerSaml(app);
// File upload/download routes // File upload/download routes
await fileRoutes(app); await fileRoutes(app);
@@ -417,6 +421,23 @@ app.get("/api/v1/config/auth", async () => {
config.oidcProviderName = env.OIDC_PROVIDER_NAME || null; config.oidcProviderName = env.OIDC_PROVIDER_NAME || null;
config.oidcLoginUrl = "/api/auth/oidc/login"; config.oidcLoginUrl = "/api/auth/oidc/login";
} }
// SAML SSO requires both env flag and enterprise license
let samlLicensed = false;
if (env.SAML_ENABLED) {
try {
const { isFeatureEnabled } = await import("@snapotter/enterprise");
samlLicensed = isFeatureEnabled("saml_sso");
} catch {
// Enterprise package not available
}
}
if (env.SAML_ENABLED && samlLicensed) {
config.samlEnabled = true;
config.samlProviderName = env.SAML_PROVIDER_NAME || "SSO";
config.samlLoginUrl = "/api/auth/saml/login";
}
return config; return config;
}); });
+43
View File
@@ -79,6 +79,26 @@ const envSchema = z
OIDC_PROVIDER_NAME: z.string().default(""), OIDC_PROVIDER_NAME: z.string().default(""),
OIDC_CLOCK_TOLERANCE: z.coerce.number().min(0).max(300).default(30), OIDC_CLOCK_TOLERANCE: z.coerce.number().min(0).max(300).default(30),
OIDC_USERNAME_CLAIM: z.string().default("preferred_username"), OIDC_USERNAME_CLAIM: z.string().default("preferred_username"),
SAML_ENABLED: z
.enum(["true", "false"])
.default("false")
.transform((v) => v === "true"),
SAML_ENTITY_ID: z.string().default(""),
SAML_CALLBACK_URL: z.string().default(""),
SAML_IDP_SSO_URL: z.string().default(""),
SAML_IDP_CERTIFICATE: z.string().default(""),
SAML_AUTO_CREATE_USERS: z
.enum(["true", "false"])
.default("true")
.transform((v) => v === "true"),
SAML_AUTO_LINK_USERS: z
.enum(["true", "false"])
.default("false")
.transform((v) => v === "true"),
SAML_DEFAULT_ROLE: z.string().default("user"),
SAML_PROVIDER_NAME: z.string().default(""),
SAML_USERNAME_ATTRIBUTE: z.string().default(""),
SAML_EMAIL_ATTRIBUTE: z.string().default("email"),
EXTERNAL_URL: z.string().default(""), EXTERNAL_URL: z.string().default(""),
COOKIE_SECRET: z.string().default(""), COOKIE_SECRET: z.string().default(""),
REDIS_URL: z.string().default("redis://localhost:6379"), REDIS_URL: z.string().default("redis://localhost:6379"),
@@ -154,6 +174,29 @@ const envSchema = z
}); });
} }
} }
if (data.SAML_ENABLED) {
if (!data.SAML_IDP_SSO_URL) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "SAML_IDP_SSO_URL is required when SAML_ENABLED=true",
path: ["SAML_IDP_SSO_URL"],
});
}
if (!data.SAML_IDP_CERTIFICATE) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "SAML_IDP_CERTIFICATE is required when SAML_ENABLED=true",
path: ["SAML_IDP_CERTIFICATE"],
});
}
if (!data.EXTERNAL_URL) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "EXTERNAL_URL is required when SAML_ENABLED=true",
path: ["EXTERNAL_URL"],
});
}
}
}); });
export type Env = z.infer<typeof envSchema>; export type Env = z.infer<typeof envSchema>;
+1 -1
View File
@@ -458,7 +458,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
mustChangePassword: env.SKIP_MUST_CHANGE_PASSWORD ? false : user.mustChangePassword, mustChangePassword: env.SKIP_MUST_CHANGE_PASSWORD ? false : user.mustChangePassword,
permissions: await getPermissions(user.role), permissions: await getPermissions(user.role),
authProvider: user.authProvider ?? "local", authProvider: user.authProvider ?? "local",
loginMethod: session.idToken ? "oidc" : "local", loginMethod: session.idToken ? "oidc" : user.authProvider === "saml" ? "saml" : "local",
email: user.email ?? null, email: user.email ?? null,
hasLocalPassword: !!user.passwordHash, hasLocalPassword: !!user.passwordHash,
hasOidcLink: !!user.externalId, hasOidcLink: !!user.externalId,
+178
View File
@@ -0,0 +1,178 @@
import { parse as parseQs } from "node:querystring";
import type {} from "@fastify/cookie";
import { SAML } from "@node-saml/node-saml";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { env } from "../config.js";
import { db, schema } from "../db/index.js";
import { auditFromRequest } from "../lib/audit.js";
import {
findUniqueUsername,
resolveExternalUser,
sanitizeUsername,
} from "../lib/external-auth-resolver.js";
import { createSessionToken } from "./auth.js";
// -- SAML instance factory ----------------------------------------------------
function getSamlInstance(): SAML {
return new SAML({
callbackUrl: env.SAML_CALLBACK_URL || `${env.EXTERNAL_URL}/api/auth/saml/callback`,
entryPoint: env.SAML_IDP_SSO_URL,
issuer: env.SAML_ENTITY_ID || `${env.EXTERNAL_URL}/api/auth/saml/metadata`,
cert: env.SAML_IDP_CERTIFICATE,
wantAuthnResponseSigned: true,
wantAssertionsSigned: true,
});
}
// -- Helpers ------------------------------------------------------------------
function isSecure(): boolean {
return env.EXTERNAL_URL.startsWith("https");
}
const SESSION_DURATION_MS = env.SESSION_DURATION_HOURS * 60 * 60 * 1000;
function redirectToLogin(reply: FastifyReply, errorCode: string): void {
reply.redirect(`/login?error=${errorCode}`);
}
// -- Plugin registration ------------------------------------------------------
export async function registerSaml(app: FastifyInstance): Promise<void> {
if (!env.SAML_ENABLED) return;
let isEnabled = false;
try {
const { isFeatureEnabled } = await import("@snapotter/enterprise");
isEnabled = isFeatureEnabled("saml_sso");
} catch {
// Enterprise package not available
}
if (!isEnabled) {
app.log.warn("SAML is enabled via env but saml_sso enterprise feature is not licensed");
return;
}
// Register form-urlencoded content type parser for the SAML callback.
// The IdP POSTs the SAML response as application/x-www-form-urlencoded.
app.addContentTypeParser(
"application/x-www-form-urlencoded",
{ parseAs: "string" },
(_request, body, done) => {
try {
const str = typeof body === "string" ? body : (body as Buffer).toString();
done(null, parseQs(str));
} catch (err) {
done(err as Error, undefined);
}
},
);
// GET /api/auth/saml/metadata -- SP metadata XML
app.get("/api/auth/saml/metadata", async (_request: FastifyRequest, reply: FastifyReply) => {
const saml = getSamlInstance();
const metadata = saml.generateServiceProviderMetadata(null, null);
return reply.type("application/xml").send(metadata);
});
// GET /api/auth/saml/login -- SP-initiated login redirect
app.get("/api/auth/saml/login", async (_request: FastifyRequest, reply: FastifyReply) => {
try {
const saml = getSamlInstance();
const loginUrl = await saml.getAuthorizeUrlAsync("", undefined, {});
return reply.redirect(loginUrl);
} catch (err) {
_request.log.error({ err }, "SAML login redirect failed");
return redirectToLogin(reply, "saml_auth_failed");
}
});
// POST /api/auth/saml/callback -- Assertion Consumer Service (ACS)
app.post("/api/auth/saml/callback", async (request: FastifyRequest, reply: FastifyReply) => {
const saml = getSamlInstance();
const audit = auditFromRequest(request);
let profile;
try {
const result = await saml.validatePostResponseAsync(request.body as Record<string, string>);
profile = result.profile;
} catch (err) {
request.log.error({ err }, "SAML assertion validation failed");
await audit("SAML_LOGIN_FAILED", {
error: err instanceof Error ? err.message : "Unknown error",
});
return redirectToLogin(reply, "saml_auth_failed");
}
if (!profile || !profile.nameID) {
request.log.warn("SAML callback: no profile or nameID in assertion");
await audit("SAML_LOGIN_FAILED", { reason: "missing_profile" });
return redirectToLogin(reply, "saml_auth_failed");
}
// Extract claims from SAML assertion
const externalId = profile.nameID;
const email = profile[env.SAML_EMAIL_ATTRIBUTE] as string | undefined;
const usernameAttr = env.SAML_USERNAME_ATTRIBUTE
? (profile[env.SAML_USERNAME_ATTRIBUTE] as string | undefined)
: undefined;
// Derive a username from available claims
const rawUsername = usernameAttr || email?.split("@")[0] || profile.nameID;
let username = sanitizeUsername(rawUsername);
username = await findUniqueUsername(username);
// Resolve user via shared external-auth resolver
const result = await resolveExternalUser({
provider: "saml",
externalId,
email,
emailVerified: true, // SAML assertions from a trusted IdP are considered verified
username,
autoCreate: env.SAML_AUTO_CREATE_USERS,
autoLink: env.SAML_AUTO_LINK_USERS,
defaultRole: env.SAML_DEFAULT_ROLE,
logger: request.log,
ip: request.ip,
requestId: request.id,
});
if (result.action === "denied" || !result.user) {
const errorParam =
result.deniedReason === "user_limit_reached"
? "saml_user_limit_reached"
: "saml_user_not_authorized";
return redirectToLogin(reply, errorParam);
}
const resolvedUser = result.user;
// Create session (same pattern as OIDC)
const token = createSessionToken();
const expiresAt = new Date(Date.now() + SESSION_DURATION_MS);
await db.insert(schema.sessions).values({
id: token,
userId: resolvedUser.id,
expiresAt,
});
await audit("SAML_LOGIN_SUCCESS", {
userId: resolvedUser.id,
username: resolvedUser.username,
});
// Set session cookie and redirect to app
reply.setCookie("snapotter-session", token, {
httpOnly: true,
sameSite: "strict",
secure: isSecure(),
path: "/",
maxAge: env.SESSION_DURATION_HOURS * 3600,
});
return reply.redirect("/");
});
}
+10
View File
@@ -14,6 +14,8 @@ interface AuthState {
analyticsConsentRemindAt: number | null; analyticsConsentRemindAt: number | null;
oidcEnabled: boolean; oidcEnabled: boolean;
oidcProviderName: string | null; oidcProviderName: string | null;
samlEnabled: boolean;
samlProviderName: string | null;
loginMethod: string | null; loginMethod: string | null;
hasLocalPassword: boolean; hasLocalPassword: boolean;
} }
@@ -48,6 +50,8 @@ export function useAuth() {
analyticsConsentRemindAt: null, analyticsConsentRemindAt: null,
oidcEnabled: false, oidcEnabled: false,
oidcProviderName: null, oidcProviderName: null,
samlEnabled: false,
samlProviderName: null,
loginMethod: null, loginMethod: null,
hasLocalPassword: false, hasLocalPassword: false,
}); });
@@ -74,6 +78,8 @@ export function useAuth() {
analyticsConsentRemindAt: null, analyticsConsentRemindAt: null,
oidcEnabled: false, oidcEnabled: false,
oidcProviderName: null, oidcProviderName: null,
samlEnabled: false,
samlProviderName: null,
loginMethod: null, loginMethod: null,
hasLocalPassword: false, hasLocalPassword: false,
}); });
@@ -102,6 +108,8 @@ export function useAuth() {
analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? null, analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? null,
oidcEnabled: config.oidcEnabled ?? false, oidcEnabled: config.oidcEnabled ?? false,
oidcProviderName: config.oidcProviderName ?? null, oidcProviderName: config.oidcProviderName ?? null,
samlEnabled: config.samlEnabled ?? false,
samlProviderName: config.samlProviderName ?? null,
loginMethod: session.user?.loginMethod ?? null, loginMethod: session.user?.loginMethod ?? null,
hasLocalPassword: session.user?.hasLocalPassword ?? false, hasLocalPassword: session.user?.hasLocalPassword ?? false,
}); });
@@ -120,6 +128,8 @@ export function useAuth() {
analyticsConsentRemindAt: null, analyticsConsentRemindAt: null,
oidcEnabled: config.oidcEnabled ?? false, oidcEnabled: config.oidcEnabled ?? false,
oidcProviderName: config.oidcProviderName ?? null, oidcProviderName: config.oidcProviderName ?? null,
samlEnabled: config.samlEnabled ?? false,
samlProviderName: config.samlProviderName ?? null,
loginMethod: null, loginMethod: null,
hasLocalPassword: false, hasLocalPassword: false,
}); });
+18 -5
View File
@@ -127,7 +127,7 @@ function LanguageSelector() {
export function LoginPage() { export function LoginPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { oidcEnabled, oidcProviderName } = useAuth(); const { oidcEnabled, oidcProviderName, samlEnabled, samlProviderName } = useAuth();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -135,16 +135,19 @@ export function LoginPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
const oidcError = searchParams.get("error"); const authError = searchParams.get("error");
if (oidcError) { if (authError) {
const errorMessages: Record<string, string> = { const errorMessages: Record<string, string> = {
oidc_auth_failed: t.auth.oidcAuthFailed, oidc_auth_failed: t.auth.oidcAuthFailed,
oidc_provider_unreachable: t.auth.oidcProviderUnreachable, oidc_provider_unreachable: t.auth.oidcProviderUnreachable,
oidc_session_expired: t.auth.oidcSessionExpired, oidc_session_expired: t.auth.oidcSessionExpired,
oidc_user_not_authorized: t.auth.oidcUserNotAuthorized, oidc_user_not_authorized: t.auth.oidcUserNotAuthorized,
oidc_user_limit_reached: t.auth.oidcUserLimitReached, oidc_user_limit_reached: t.auth.oidcUserLimitReached,
saml_auth_failed: t.auth.samlAuthFailed,
saml_user_not_authorized: t.auth.samlUserNotAuthorized,
saml_user_limit_reached: t.auth.samlUserLimitReached,
}; };
setError(errorMessages[oidcError] || t.auth.oidcGenericError); setError(errorMessages[authError] || t.auth.oidcGenericError);
} }
}, [searchParams, t]); }, [searchParams, t]);
@@ -230,19 +233,29 @@ export function LoginPage() {
{loading ? t.auth.loggingIn : t.auth.loginButton} {loading ? t.auth.loggingIn : t.auth.loginButton}
</button> </button>
</form> </form>
{oidcEnabled && ( {(oidcEnabled || samlEnabled) && (
<> <>
<div className="flex items-center gap-3 my-4"> <div className="flex items-center gap-3 my-4">
<div className="flex-1 border-t border-border" /> <div className="flex-1 border-t border-border" />
<span className="text-sm text-muted-foreground">{t.auth.or}</span> <span className="text-sm text-muted-foreground">{t.auth.or}</span>
<div className="flex-1 border-t border-border" /> <div className="flex-1 border-t border-border" />
</div> </div>
{oidcEnabled && (
<a <a
href="/api/auth/oidc/login" href="/api/auth/oidc/login"
className="w-full py-3 rounded-lg bg-secondary text-secondary-foreground font-medium hover:bg-secondary/80 transition-colors flex items-center justify-center gap-2" className="w-full py-3 rounded-lg bg-secondary text-secondary-foreground font-medium hover:bg-secondary/80 transition-colors flex items-center justify-center gap-2"
> >
{format(t.auth.signInWith, { provider: oidcProviderName || "SSO" })} {format(t.auth.signInWith, { provider: oidcProviderName || "SSO" })}
</a> </a>
)}
{samlEnabled && (
<a
href="/api/auth/saml/login"
className="w-full py-3 rounded-lg bg-secondary text-secondary-foreground font-medium hover:bg-secondary/80 transition-colors flex items-center justify-center gap-2 mt-2"
>
{format(t.auth.signInWith, { provider: samlProviderName || "SSO" })}
</a>
)}
</> </>
)} )}
<div className="pt-2"> <div className="pt-2">
+5
View File
@@ -2997,6 +2997,11 @@ export const ar: TranslationKeys = {
oidcUserNotAuthorized: "حسابك غير مصرح له بالوصول إلى هذا التطبيق. تواصل مع المسؤول.", oidcUserNotAuthorized: "حسابك غير مصرح له بالوصول إلى هذا التطبيق. تواصل مع المسؤول.",
oidcUserLimitReached: "تم الوصول لحد المستخدمين. تواصل مع المسؤول.", oidcUserLimitReached: "تم الوصول لحد المستخدمين. تواصل مع المسؤول.",
oidcGenericError: "خطأ في المصادقة. يرجى المحاولة مرة أخرى.", oidcGenericError: "خطأ في المصادقة. يرجى المحاولة مرة أخرى.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "يتم إدارة تغيير كلمة المرور بواسطة مزود الهوية الخاص بك.", passwordManagedByProvider: "يتم إدارة تغيير كلمة المرور بواسطة مزود الهوية الخاص بك.",
enterUsername: "أدخل اسم المستخدم", enterUsername: "أدخل اسم المستخدم",
enterPassword: "أدخل كلمة المرور", enterPassword: "أدخل كلمة المرور",
+5
View File
@@ -3024,6 +3024,11 @@ export const de: TranslationKeys = {
"Ihr Konto ist nicht fuer den Zugriff auf diese Anwendung autorisiert. Kontaktieren Sie Ihren Administrator.", "Ihr Konto ist nicht fuer den Zugriff auf diese Anwendung autorisiert. Kontaktieren Sie Ihren Administrator.",
oidcUserLimitReached: "Benutzerlimit erreicht. Kontaktieren Sie Ihren Administrator.", oidcUserLimitReached: "Benutzerlimit erreicht. Kontaktieren Sie Ihren Administrator.",
oidcGenericError: "Authentifizierungsfehler. Bitte versuchen Sie es erneut.", oidcGenericError: "Authentifizierungsfehler. Bitte versuchen Sie es erneut.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: passwordManagedByProvider:
"Passwortaenderungen werden von Ihrem Identitaetsanbieter verwaltet.", "Passwortaenderungen werden von Ihrem Identitaetsanbieter verwaltet.",
enterUsername: "Benutzernamen eingeben", enterUsername: "Benutzernamen eingeben",
+5
View File
@@ -2963,6 +2963,11 @@ export const en = {
"Your account is not authorized to access this application. Contact your administrator.", "Your account is not authorized to access this application. Contact your administrator.",
oidcUserLimitReached: "User limit reached. Contact your administrator.", oidcUserLimitReached: "User limit reached. Contact your administrator.",
oidcGenericError: "Authentication error. Please try again.", oidcGenericError: "Authentication error. Please try again.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Password changes are managed by your identity provider.", passwordManagedByProvider: "Password changes are managed by your identity provider.",
enterUsername: "Enter username", enterUsername: "Enter username",
enterPassword: "Enter your password", enterPassword: "Enter your password",
+5
View File
@@ -3002,6 +3002,11 @@ export const es: TranslationKeys = {
"Tu cuenta no esta autorizada para acceder a esta aplicacion. Contacta a tu administrador.", "Tu cuenta no esta autorizada para acceder a esta aplicacion. Contacta a tu administrador.",
oidcUserLimitReached: "Limite de usuarios alcanzado. Contacta a tu administrador.", oidcUserLimitReached: "Limite de usuarios alcanzado. Contacta a tu administrador.",
oidcGenericError: "Error de autenticacion. Intenta de nuevo.", oidcGenericError: "Error de autenticacion. Intenta de nuevo.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: passwordManagedByProvider:
"Los cambios de contrasena son administrados por tu proveedor de identidad.", "Los cambios de contrasena son administrados por tu proveedor de identidad.",
enterUsername: "Ingresa tu nombre de usuario", enterUsername: "Ingresa tu nombre de usuario",
+5
View File
@@ -3022,6 +3022,11 @@ export const fr: TranslationKeys = {
"Votre compte n'est pas autorise a acceder a cette application. Contactez votre administrateur.", "Votre compte n'est pas autorise a acceder a cette application. Contactez votre administrateur.",
oidcUserLimitReached: "Limite d'utilisateurs atteinte. Contactez votre administrateur.", oidcUserLimitReached: "Limite d'utilisateurs atteinte. Contactez votre administrateur.",
oidcGenericError: "Erreur d'authentification. Veuillez reessayer.", oidcGenericError: "Erreur d'authentification. Veuillez reessayer.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: passwordManagedByProvider:
"Les modifications de mot de passe sont gerees par votre fournisseur d'identite.", "Les modifications de mot de passe sont gerees par votre fournisseur d'identite.",
enterUsername: "Saisissez votre nom d'utilisateur", enterUsername: "Saisissez votre nom d'utilisateur",
+5
View File
@@ -2994,6 +2994,11 @@ export const hi: TranslationKeys = {
"आपके अकाउंट को इस एप्लिकेशन तक पहुंचने की अनुमति नहीं है। अपने एडमिनिस्ट्रेटर से संपर्क करें।", "आपके अकाउंट को इस एप्लिकेशन तक पहुंचने की अनुमति नहीं है। अपने एडमिनिस्ट्रेटर से संपर्क करें।",
oidcUserLimitReached: "उपयोगकर्ता सीमा पहुंच गई। अपने एडमिनिस्ट्रेटर से संपर्क करें।", oidcUserLimitReached: "उपयोगकर्ता सीमा पहुंच गई। अपने एडमिनिस्ट्रेटर से संपर्क करें।",
oidcGenericError: "ऑथेंटिकेशन त्रुटि। कृपया पुनः प्रयास करें।", oidcGenericError: "ऑथेंटिकेशन त्रुटि। कृपया पुनः प्रयास करें।",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "पासवर्ड बदलाव आपके आइडेंटिटी प्रोवाइडर द्वारा प्रबंधित किए जाते हैं।", passwordManagedByProvider: "पासवर्ड बदलाव आपके आइडेंटिटी प्रोवाइडर द्वारा प्रबंधित किए जाते हैं।",
enterUsername: "यूज़रनेम दर्ज करें", enterUsername: "यूज़रनेम दर्ज करें",
enterPassword: "अपना पासवर्ड दर्ज करें", enterPassword: "अपना पासवर्ड दर्ज करें",
+5
View File
@@ -3010,6 +3010,11 @@ export const id: TranslationKeys = {
"Akun Anda tidak diizinkan mengakses aplikasi ini. Hubungi administrator Anda.", "Akun Anda tidak diizinkan mengakses aplikasi ini. Hubungi administrator Anda.",
oidcUserLimitReached: "Batas pengguna tercapai. Hubungi administrator Anda.", oidcUserLimitReached: "Batas pengguna tercapai. Hubungi administrator Anda.",
oidcGenericError: "Kesalahan autentikasi. Silakan coba lagi.", oidcGenericError: "Kesalahan autentikasi. Silakan coba lagi.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Perubahan kata sandi dikelola oleh penyedia identitas Anda.", passwordManagedByProvider: "Perubahan kata sandi dikelola oleh penyedia identitas Anda.",
enterUsername: "Masukkan nama pengguna", enterUsername: "Masukkan nama pengguna",
enterPassword: "Masukkan kata sandi Anda", enterPassword: "Masukkan kata sandi Anda",
+5
View File
@@ -3016,6 +3016,11 @@ export const it: TranslationKeys = {
"Il tuo account non e autorizzato ad accedere a questa applicazione. Contatta l'amministratore.", "Il tuo account non e autorizzato ad accedere a questa applicazione. Contatta l'amministratore.",
oidcUserLimitReached: "Limite utenti raggiunto. Contatta l'amministratore.", oidcUserLimitReached: "Limite utenti raggiunto. Contatta l'amministratore.",
oidcGenericError: "Errore di autenticazione. Riprova.", oidcGenericError: "Errore di autenticazione. Riprova.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: passwordManagedByProvider:
"Le modifiche alla password sono gestite dal tuo provider di identita.", "Le modifiche alla password sono gestite dal tuo provider di identita.",
enterUsername: "Inserisci il nome utente", enterUsername: "Inserisci il nome utente",
+5
View File
@@ -2967,6 +2967,11 @@ export const ja: TranslationKeys = {
"お使いのアカウントにはこのアプリケーションへのアクセス権がありません。管理者にお問い合わせください。", "お使いのアカウントにはこのアプリケーションへのアクセス権がありません。管理者にお問い合わせください。",
oidcUserLimitReached: "ユーザー上限に達しました。管理者にお問い合わせください。", oidcUserLimitReached: "ユーザー上限に達しました。管理者にお問い合わせください。",
oidcGenericError: "認証エラー。もう一度お試しください。", oidcGenericError: "認証エラー。もう一度お試しください。",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "パスワードはIDプロバイダーで管理されています。", passwordManagedByProvider: "パスワードはIDプロバイダーで管理されています。",
enterUsername: "ユーザー名を入力", enterUsername: "ユーザー名を入力",
enterPassword: "パスワードを入力", enterPassword: "パスワードを入力",
+5
View File
@@ -2952,6 +2952,11 @@ export const ko: TranslationKeys = {
"귀하의 계정은 이 애플리케이션에 접근할 권한이 없습니다. 관리자에게 문의하세요.", "귀하의 계정은 이 애플리케이션에 접근할 권한이 없습니다. 관리자에게 문의하세요.",
oidcUserLimitReached: "사용자 한도에 도달했습니다. 관리자에게 문의하세요.", oidcUserLimitReached: "사용자 한도에 도달했습니다. 관리자에게 문의하세요.",
oidcGenericError: "인증 오류. 다시 시도해 주세요.", oidcGenericError: "인증 오류. 다시 시도해 주세요.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "비밀번호는 ID 제공자에서 관리됩니다.", passwordManagedByProvider: "비밀번호는 ID 제공자에서 관리됩니다.",
enterUsername: "사용자명 입력", enterUsername: "사용자명 입력",
enterPassword: "비밀번호 입력", enterPassword: "비밀번호 입력",
+5
View File
@@ -3013,6 +3013,11 @@ export const nl: TranslationKeys = {
"Je account heeft geen toegang tot deze applicatie. Neem contact op met je beheerder.", "Je account heeft geen toegang tot deze applicatie. Neem contact op met je beheerder.",
oidcUserLimitReached: "Gebruikerslimiet bereikt. Neem contact op met je beheerder.", oidcUserLimitReached: "Gebruikerslimiet bereikt. Neem contact op met je beheerder.",
oidcGenericError: "Authenticatiefout. Probeer het opnieuw.", oidcGenericError: "Authenticatiefout. Probeer het opnieuw.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Wachtwoordwijzigingen worden beheerd door je identiteitsprovider.", passwordManagedByProvider: "Wachtwoordwijzigingen worden beheerd door je identiteitsprovider.",
enterUsername: "Voer gebruikersnaam in", enterUsername: "Voer gebruikersnaam in",
enterPassword: "Voer je wachtwoord in", enterPassword: "Voer je wachtwoord in",
+5
View File
@@ -3020,6 +3020,11 @@ export const pl: TranslationKeys = {
"Państwa konto nie ma uprawnień do korzystania z tej aplikacji. Skontaktuj się z administratorem.", "Państwa konto nie ma uprawnień do korzystania z tej aplikacji. Skontaktuj się z administratorem.",
oidcUserLimitReached: "Osiągnięto limit użytkowników. Skontaktuj się z administratorem.", oidcUserLimitReached: "Osiągnięto limit użytkowników. Skontaktuj się z administratorem.",
oidcGenericError: "Błąd uwierzytelniania. Spróbuj ponownie.", oidcGenericError: "Błąd uwierzytelniania. Spróbuj ponownie.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Zarządzanie hasłem odbywa się przez dostawcę tożsamości.", passwordManagedByProvider: "Zarządzanie hasłem odbywa się przez dostawcę tożsamości.",
enterUsername: "Wprowadź nazwę użytkownika", enterUsername: "Wprowadź nazwę użytkownika",
enterPassword: "Wprowadź hasło", enterPassword: "Wprowadź hasło",
+5
View File
@@ -3013,6 +3013,11 @@ export const ptBR: TranslationKeys = {
"Sua conta nao esta autorizada a acessar este aplicativo. Entre em contato com o administrador.", "Sua conta nao esta autorizada a acessar este aplicativo. Entre em contato com o administrador.",
oidcUserLimitReached: "Limite de usuarios atingido. Entre em contato com o administrador.", oidcUserLimitReached: "Limite de usuarios atingido. Entre em contato com o administrador.",
oidcGenericError: "Erro de autenticacao. Tente novamente.", oidcGenericError: "Erro de autenticacao. Tente novamente.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: passwordManagedByProvider:
"Alteracoes de senha sao gerenciadas pelo seu provedor de identidade.", "Alteracoes de senha sao gerenciadas pelo seu provedor de identidade.",
enterUsername: "Digite seu nome de usuario", enterUsername: "Digite seu nome de usuario",
+5
View File
@@ -3012,6 +3012,11 @@ export const ru: TranslationKeys = {
"Вашей учётной записи не предоставлен доступ к этому приложению. Обратитесь к администратору.", "Вашей учётной записи не предоставлен доступ к этому приложению. Обратитесь к администратору.",
oidcUserLimitReached: "Достигнут лимит пользователей. Обратитесь к администратору.", oidcUserLimitReached: "Достигнут лимит пользователей. Обратитесь к администратору.",
oidcGenericError: "Ошибка аутентификации. Попробуйте снова.", oidcGenericError: "Ошибка аутентификации. Попробуйте снова.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Управление паролем осуществляется Вашим провайдером идентификации.", passwordManagedByProvider: "Управление паролем осуществляется Вашим провайдером идентификации.",
enterUsername: "Введите имя пользователя", enterUsername: "Введите имя пользователя",
enterPassword: "Введите пароль", enterPassword: "Введите пароль",
+5
View File
@@ -3008,6 +3008,11 @@ export const sv: TranslationKeys = {
"Ditt konto har inte behorighet till denna applikation. Kontakta din administrator.", "Ditt konto har inte behorighet till denna applikation. Kontakta din administrator.",
oidcUserLimitReached: "Anvandargrans nadd. Kontakta din administrator.", oidcUserLimitReached: "Anvandargrans nadd. Kontakta din administrator.",
oidcGenericError: "Autentiseringsfel. Forsok igen.", oidcGenericError: "Autentiseringsfel. Forsok igen.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Losenordsandringar hanteras av din identitetsleverantor.", passwordManagedByProvider: "Losenordsandringar hanteras av din identitetsleverantor.",
enterUsername: "Ange anvandarnamn", enterUsername: "Ange anvandarnamn",
enterPassword: "Ange ditt losenord", enterPassword: "Ange ditt losenord",
+5
View File
@@ -2985,6 +2985,11 @@ export const th: TranslationKeys = {
oidcUserNotAuthorized: "บัญชีของคุณไม่ได้รับอนุญาตให้เข้าถึงแอปพลิเคชันนี้ กรุณาติดต่อผู้ดูแลระบบ", oidcUserNotAuthorized: "บัญชีของคุณไม่ได้รับอนุญาตให้เข้าถึงแอปพลิเคชันนี้ กรุณาติดต่อผู้ดูแลระบบ",
oidcUserLimitReached: "ถึงจำนวนผู้ใช้สูงสุดแล้ว กรุณาติดต่อผู้ดูแลระบบ", oidcUserLimitReached: "ถึงจำนวนผู้ใช้สูงสุดแล้ว กรุณาติดต่อผู้ดูแลระบบ",
oidcGenericError: "ข้อผิดพลาดในการยืนยันตัวตน กรุณาลองอีกครั้ง", oidcGenericError: "ข้อผิดพลาดในการยืนยันตัวตน กรุณาลองอีกครั้ง",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "การเปลี่ยนรหัสผ่านจัดการโดยผู้ให้บริการยืนยันตัวตนของคุณ", passwordManagedByProvider: "การเปลี่ยนรหัสผ่านจัดการโดยผู้ให้บริการยืนยันตัวตนของคุณ",
enterUsername: "กรอกชื่อผู้ใช้", enterUsername: "กรอกชื่อผู้ใช้",
enterPassword: "กรอกรหัสผ่านของคุณ", enterPassword: "กรอกรหัสผ่านของคุณ",
+5
View File
@@ -3016,6 +3016,11 @@ export const tr: TranslationKeys = {
"Hesabınız bu uygulamaya erişim yetkisine sahip değil. Yöneticinizle iletişime geçin.", "Hesabınız bu uygulamaya erişim yetkisine sahip değil. Yöneticinizle iletişime geçin.",
oidcUserLimitReached: "Kullanıcı limitine ulaşıldı. Yöneticinizle iletişime geçin.", oidcUserLimitReached: "Kullanıcı limitine ulaşıldı. Yöneticinizle iletişime geçin.",
oidcGenericError: "Kimlik doğrulama hatası. Lütfen tekrar deneyin.", oidcGenericError: "Kimlik doğrulama hatası. Lütfen tekrar deneyin.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: passwordManagedByProvider:
"Parola değişiklikleri kimlik sağlayıcınız tarafından yönetilmektedir.", "Parola değişiklikleri kimlik sağlayıcınız tarafından yönetilmektedir.",
enterUsername: "Kullanıcı adını girin", enterUsername: "Kullanıcı adını girin",
+5
View File
@@ -3013,6 +3013,11 @@ export const uk: TranslationKeys = {
"Ваш обліковий запис не має доступу до цього застосунку. Зверніться до адміністратора.", "Ваш обліковий запис не має доступу до цього застосунку. Зверніться до адміністратора.",
oidcUserLimitReached: "Досягнуто ліміту користувачів. Зверніться до адміністратора.", oidcUserLimitReached: "Досягнуто ліміту користувачів. Зверніться до адміністратора.",
oidcGenericError: "Помилка автентифікації. Спробуйте знову.", oidcGenericError: "Помилка автентифікації. Спробуйте знову.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Керування паролем здійснюється Вашим постачальником ідентифікації.", passwordManagedByProvider: "Керування паролем здійснюється Вашим постачальником ідентифікації.",
enterUsername: "Введіть ім'я користувача", enterUsername: "Введіть ім'я користувача",
enterPassword: "Введіть пароль", enterPassword: "Введіть пароль",
+5
View File
@@ -3008,6 +3008,11 @@ export const vi: TranslationKeys = {
"Tài khoản của bạn không được phép truy cập ứng dụng này. Liên hệ quản trị viên.", "Tài khoản của bạn không được phép truy cập ứng dụng này. Liên hệ quản trị viên.",
oidcUserLimitReached: "Đã đạt giới hạn người dùng. Liên hệ quản trị viên.", oidcUserLimitReached: "Đã đạt giới hạn người dùng. Liên hệ quản trị viên.",
oidcGenericError: "Lỗi xác thực. Vui lòng thử lại.", oidcGenericError: "Lỗi xác thực. Vui lòng thử lại.",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "Việc đổi mật khẩu được quản lý bởi nhà cung cấp danh tính của bạn.", passwordManagedByProvider: "Việc đổi mật khẩu được quản lý bởi nhà cung cấp danh tính của bạn.",
enterUsername: "Nhập tên đăng nhập", enterUsername: "Nhập tên đăng nhập",
enterPassword: "Nhập mật khẩu của bạn", enterPassword: "Nhập mật khẩu của bạn",
+5
View File
@@ -2937,6 +2937,11 @@ export const zhCN: TranslationKeys = {
oidcUserNotAuthorized: "您的账号无权访问此应用。请联系管理员。", oidcUserNotAuthorized: "您的账号无权访问此应用。请联系管理员。",
oidcUserLimitReached: "用户数量已达上限。请联系管理员。", oidcUserLimitReached: "用户数量已达上限。请联系管理员。",
oidcGenericError: "认证错误。请重试。", oidcGenericError: "认证错误。请重试。",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "密码修改由您的身份提供商管理。", passwordManagedByProvider: "密码修改由您的身份提供商管理。",
enterUsername: "输入用户名", enterUsername: "输入用户名",
enterPassword: "输入密码", enterPassword: "输入密码",
+5
View File
@@ -2935,6 +2935,11 @@ export const zhTW: TranslationKeys = {
oidcUserNotAuthorized: "您的帳戶無權存取此應用程式。請聯絡管理員。", oidcUserNotAuthorized: "您的帳戶無權存取此應用程式。請聯絡管理員。",
oidcUserLimitReached: "已達使用者上限,請聯絡管理員。", oidcUserLimitReached: "已達使用者上限,請聯絡管理員。",
oidcGenericError: "驗證錯誤,請重試。", oidcGenericError: "驗證錯誤,請重試。",
samlAuthFailed: "SAML authentication failed. Please try again.",
samlUserNotAuthorized:
"Your account is not authorized to access this application. Contact your administrator.",
samlUserLimitReached: "User limit reached. Contact your administrator.",
methodSaml: "SAML",
passwordManagedByProvider: "密碼由您的身分提供者管理。", passwordManagedByProvider: "密碼由您的身分提供者管理。",
enterUsername: "輸入使用者名稱", enterUsername: "輸入使用者名稱",
enterPassword: "輸入密碼", enterPassword: "輸入密碼",