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,
} from "./plugins/auth.js";
import { oidcRoutes } from "./plugins/oidc.js";
import { registerSaml } from "./plugins/saml.js";
import { registerStatic } from "./plugins/static.js";
import { registerUpload } from "./plugins/upload.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 { configRoutes } from "./routes/config.js";
import { docsRoutes } from "./routes/docs.js";
import { registerEnterpriseRoutes } from "./routes/enterprise/index.js";
import { registerFeatureRoutes } from "./routes/features.js";
import { registerFetchUrlsRoute } from "./routes/fetch-urls.js";
import { fileRoutes } from "./routes/files.js";
@@ -50,7 +52,6 @@ import { settingsRoutes } from "./routes/settings.js";
import { teamsRoutes } from "./routes/teams.js";
import { registerToolRoutes } from "./routes/tools/index.js";
import { userFileRoutes } from "./routes/user-files.js";
import { registerEnterpriseRoutes } from "./routes/enterprise/index.js";
// Run before anything else
try {
@@ -302,6 +303,9 @@ await authRoutes(app);
// OIDC routes
await oidcRoutes(app);
// SAML routes
await registerSaml(app);
// File upload/download routes
await fileRoutes(app);
@@ -417,6 +421,23 @@ app.get("/api/v1/config/auth", async () => {
config.oidcProviderName = env.OIDC_PROVIDER_NAME || null;
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;
});
+43
View File
@@ -79,6 +79,26 @@ const envSchema = z
OIDC_PROVIDER_NAME: z.string().default(""),
OIDC_CLOCK_TOLERANCE: z.coerce.number().min(0).max(300).default(30),
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(""),
COOKIE_SECRET: z.string().default(""),
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>;
+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,
permissions: await getPermissions(user.role),
authProvider: user.authProvider ?? "local",
loginMethod: session.idToken ? "oidc" : "local",
loginMethod: session.idToken ? "oidc" : user.authProvider === "saml" ? "saml" : "local",
email: user.email ?? null,
hasLocalPassword: !!user.passwordHash,
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;
oidcEnabled: boolean;
oidcProviderName: string | null;
samlEnabled: boolean;
samlProviderName: string | null;
loginMethod: string | null;
hasLocalPassword: boolean;
}
@@ -48,6 +50,8 @@ export function useAuth() {
analyticsConsentRemindAt: null,
oidcEnabled: false,
oidcProviderName: null,
samlEnabled: false,
samlProviderName: null,
loginMethod: null,
hasLocalPassword: false,
});
@@ -74,6 +78,8 @@ export function useAuth() {
analyticsConsentRemindAt: null,
oidcEnabled: false,
oidcProviderName: null,
samlEnabled: false,
samlProviderName: null,
loginMethod: null,
hasLocalPassword: false,
});
@@ -102,6 +108,8 @@ export function useAuth() {
analyticsConsentRemindAt: session.user?.analyticsConsentRemindAt ?? null,
oidcEnabled: config.oidcEnabled ?? false,
oidcProviderName: config.oidcProviderName ?? null,
samlEnabled: config.samlEnabled ?? false,
samlProviderName: config.samlProviderName ?? null,
loginMethod: session.user?.loginMethod ?? null,
hasLocalPassword: session.user?.hasLocalPassword ?? false,
});
@@ -120,6 +128,8 @@ export function useAuth() {
analyticsConsentRemindAt: null,
oidcEnabled: config.oidcEnabled ?? false,
oidcProviderName: config.oidcProviderName ?? null,
samlEnabled: config.samlEnabled ?? false,
samlProviderName: config.samlProviderName ?? null,
loginMethod: null,
hasLocalPassword: false,
});
+24 -11
View File
@@ -127,7 +127,7 @@ function LanguageSelector() {
export function LoginPage() {
const { t } = useTranslation();
const { oidcEnabled, oidcProviderName } = useAuth();
const { oidcEnabled, oidcProviderName, samlEnabled, samlProviderName } = useAuth();
const [searchParams] = useSearchParams();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -135,16 +135,19 @@ export function LoginPage() {
const [loading, setLoading] = useState(false);
useEffect(() => {
const oidcError = searchParams.get("error");
if (oidcError) {
const authError = searchParams.get("error");
if (authError) {
const errorMessages: Record<string, string> = {
oidc_auth_failed: t.auth.oidcAuthFailed,
oidc_provider_unreachable: t.auth.oidcProviderUnreachable,
oidc_session_expired: t.auth.oidcSessionExpired,
oidc_user_not_authorized: t.auth.oidcUserNotAuthorized,
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]);
@@ -230,19 +233,29 @@ export function LoginPage() {
{loading ? t.auth.loggingIn : t.auth.loginButton}
</button>
</form>
{oidcEnabled && (
{(oidcEnabled || samlEnabled) && (
<>
<div className="flex items-center gap-3 my-4">
<div className="flex-1 border-t border-border" />
<span className="text-sm text-muted-foreground">{t.auth.or}</span>
<div className="flex-1 border-t border-border" />
</div>
<a
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"
>
{format(t.auth.signInWith, { provider: oidcProviderName || "SSO" })}
</a>
{oidcEnabled && (
<a
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"
>
{format(t.auth.signInWith, { provider: oidcProviderName || "SSO" })}
</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">
+5
View File
@@ -2997,6 +2997,11 @@ export const ar: TranslationKeys = {
oidcUserNotAuthorized: "حسابك غير مصرح له بالوصول إلى هذا التطبيق. تواصل مع المسؤول.",
oidcUserLimitReached: "تم الوصول لحد المستخدمين. تواصل مع المسؤول.",
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: "يتم إدارة تغيير كلمة المرور بواسطة مزود الهوية الخاص بك.",
enterUsername: "أدخل اسم المستخدم",
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.",
oidcUserLimitReached: "Benutzerlimit erreicht. Kontaktieren Sie Ihren Administrator.",
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:
"Passwortaenderungen werden von Ihrem Identitaetsanbieter verwaltet.",
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.",
oidcUserLimitReached: "User limit reached. Contact your administrator.",
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.",
enterUsername: "Enter username",
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.",
oidcUserLimitReached: "Limite de usuarios alcanzado. Contacta a tu administrador.",
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:
"Los cambios de contrasena son administrados por tu proveedor de identidad.",
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.",
oidcUserLimitReached: "Limite d'utilisateurs atteinte. Contactez votre administrateur.",
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:
"Les modifications de mot de passe sont gerees par votre fournisseur d'identite.",
enterUsername: "Saisissez votre nom d'utilisateur",
+5
View File
@@ -2994,6 +2994,11 @@ export const hi: TranslationKeys = {
"आपके अकाउंट को इस एप्लिकेशन तक पहुंचने की अनुमति नहीं है। अपने एडमिनिस्ट्रेटर से संपर्क करें।",
oidcUserLimitReached: "उपयोगकर्ता सीमा पहुंच गई। अपने एडमिनिस्ट्रेटर से संपर्क करें।",
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: "पासवर्ड बदलाव आपके आइडेंटिटी प्रोवाइडर द्वारा प्रबंधित किए जाते हैं।",
enterUsername: "यूज़रनेम दर्ज करें",
enterPassword: "अपना पासवर्ड दर्ज करें",
+5
View File
@@ -3010,6 +3010,11 @@ export const id: TranslationKeys = {
"Akun Anda tidak diizinkan mengakses aplikasi ini. Hubungi administrator Anda.",
oidcUserLimitReached: "Batas pengguna tercapai. Hubungi administrator Anda.",
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.",
enterUsername: "Masukkan nama pengguna",
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.",
oidcUserLimitReached: "Limite utenti raggiunto. Contatta l'amministratore.",
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:
"Le modifiche alla password sono gestite dal tuo provider di identita.",
enterUsername: "Inserisci il nome utente",
+5
View File
@@ -2967,6 +2967,11 @@ export const ja: TranslationKeys = {
"お使いのアカウントにはこのアプリケーションへのアクセス権がありません。管理者にお問い合わせください。",
oidcUserLimitReached: "ユーザー上限に達しました。管理者にお問い合わせください。",
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プロバイダーで管理されています。",
enterUsername: "ユーザー名を入力",
enterPassword: "パスワードを入力",
+5
View File
@@ -2952,6 +2952,11 @@ export const ko: TranslationKeys = {
"귀하의 계정은 이 애플리케이션에 접근할 권한이 없습니다. 관리자에게 문의하세요.",
oidcUserLimitReached: "사용자 한도에 도달했습니다. 관리자에게 문의하세요.",
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 제공자에서 관리됩니다.",
enterUsername: "사용자명 입력",
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.",
oidcUserLimitReached: "Gebruikerslimiet bereikt. Neem contact op met je beheerder.",
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.",
enterUsername: "Voer gebruikersnaam 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.",
oidcUserLimitReached: "Osiągnięto limit użytkowników. Skontaktuj się z administratorem.",
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.",
enterUsername: "Wprowadź nazwę użytkownika",
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.",
oidcUserLimitReached: "Limite de usuarios atingido. Entre em contato com o administrador.",
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:
"Alteracoes de senha sao gerenciadas pelo seu provedor de identidade.",
enterUsername: "Digite seu nome de usuario",
+5
View File
@@ -3012,6 +3012,11 @@ export const ru: TranslationKeys = {
"Вашей учётной записи не предоставлен доступ к этому приложению. Обратитесь к администратору.",
oidcUserLimitReached: "Достигнут лимит пользователей. Обратитесь к администратору.",
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: "Управление паролем осуществляется Вашим провайдером идентификации.",
enterUsername: "Введите имя пользователя",
enterPassword: "Введите пароль",
+5
View File
@@ -3008,6 +3008,11 @@ export const sv: TranslationKeys = {
"Ditt konto har inte behorighet till denna applikation. Kontakta din administrator.",
oidcUserLimitReached: "Anvandargrans nadd. Kontakta din administrator.",
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.",
enterUsername: "Ange anvandarnamn",
enterPassword: "Ange ditt losenord",
+5
View File
@@ -2985,6 +2985,11 @@ export const th: TranslationKeys = {
oidcUserNotAuthorized: "บัญชีของคุณไม่ได้รับอนุญาตให้เข้าถึงแอปพลิเคชันนี้ กรุณาติดต่อผู้ดูแลระบบ",
oidcUserLimitReached: "ถึงจำนวนผู้ใช้สูงสุดแล้ว กรุณาติดต่อผู้ดูแลระบบ",
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: "การเปลี่ยนรหัสผ่านจัดการโดยผู้ให้บริการยืนยันตัวตนของคุณ",
enterUsername: "กรอกชื่อผู้ใช้",
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.",
oidcUserLimitReached: "Kullanıcı limitine ulaşıldı. Yöneticinizle iletişime geçin.",
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:
"Parola değişiklikleri kimlik sağlayıcınız tarafından yönetilmektedir.",
enterUsername: "Kullanıcı adını girin",
+5
View File
@@ -3013,6 +3013,11 @@ export const uk: TranslationKeys = {
"Ваш обліковий запис не має доступу до цього застосунку. Зверніться до адміністратора.",
oidcUserLimitReached: "Досягнуто ліміту користувачів. Зверніться до адміністратора.",
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: "Керування паролем здійснюється Вашим постачальником ідентифікації.",
enterUsername: "Введіть ім'я користувача",
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.",
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.",
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.",
enterUsername: "Nhập tên đăng nhập",
enterPassword: "Nhập mật khẩu của bạn",
+5
View File
@@ -2937,6 +2937,11 @@ export const zhCN: TranslationKeys = {
oidcUserNotAuthorized: "您的账号无权访问此应用。请联系管理员。",
oidcUserLimitReached: "用户数量已达上限。请联系管理员。",
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: "密码修改由您的身份提供商管理。",
enterUsername: "输入用户名",
enterPassword: "输入密码",
+5
View File
@@ -2935,6 +2935,11 @@ export const zhTW: TranslationKeys = {
oidcUserNotAuthorized: "您的帳戶無權存取此應用程式。請聯絡管理員。",
oidcUserLimitReached: "已達使用者上限,請聯絡管理員。",
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: "密碼由您的身分提供者管理。",
enterUsername: "輸入使用者名稱",
enterPassword: "輸入密碼",