mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
+22
-1
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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("/");
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: "أدخل كلمة المرور",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "अपना पासवर्ड दर्ज करें",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "パスワードを入力",
|
||||
|
||||
@@ -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: "비밀번호 입력",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "Введите пароль",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "กรอกรหัสผ่านของคุณ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "Введіть пароль",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "输入密码",
|
||||
|
||||
@@ -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: "輸入密碼",
|
||||
|
||||
Reference in New Issue
Block a user