diff --git a/frontend/src/lib/i18n/i18n.ts b/frontend/src/lib/i18n/i18n.ts index eb1b809..9c50f6f 100644 --- a/frontend/src/lib/i18n/i18n.ts +++ b/frontend/src/lib/i18n/i18n.ts @@ -5,6 +5,20 @@ import ChainedBackend from "i18next-chained-backend"; import resourcesToBackend from "i18next-resources-to-backend"; import HttpBackend from "i18next-http-backend"; +const backends = [ + HttpBackend, + resourcesToBackend( + (language: string) => import(`./locales/${language}.json`), + ), +] + +const backendOptions = [ + { + loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json", + }, + {} +] + i18n .use(ChainedBackend) .use(LanguageDetector) @@ -20,17 +34,8 @@ i18n load: "currentOnly", backend: { - backends: [ - HttpBackend, - resourcesToBackend( - (language: string) => import(`./locales/${language}.json`), - ), - ], - backendOptions: [ - { - loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json", - }, - ], + backends: import.meta.env.MODE !== "development" ? backends : backends.reverse(), + backendOptions: import.meta.env.MODE !== "development" ? backendOptions : backendOptions.reverse() }, }); diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index b99ebf2..b31e2ee 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -42,5 +42,8 @@ "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", - "unauthorizedButton": "Try again" + "unauthorizedButton": "Try again", + "untrustedRedirectTitle": "Untrusted redirect", + "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", + "cancelTitle": "Cancel" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index b99ebf2..b31e2ee 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -42,5 +42,8 @@ "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", - "unauthorizedButton": "Try again" + "unauthorizedButton": "Try again", + "untrustedRedirectTitle": "Untrusted redirect", + "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", + "cancelTitle": "Cancel" } \ No newline at end of file diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx index 41582b3..8061282 100644 --- a/frontend/src/pages/continue-page.tsx +++ b/frontend/src/pages/continue-page.tsx @@ -4,7 +4,7 @@ import { Navigate } from "react-router"; import { useUserContext } from "../context/user-context"; import { Layout } from "../components/layouts/layout"; import { ReactNode } from "react"; -import { isQueryValid } from "../utils/utils"; +import { escapeRegex, isQueryValid } from "../utils/utils"; import { useAppContext } from "../context/app-context"; import { Trans, useTranslation } from "react-i18next"; @@ -14,7 +14,7 @@ export const ContinuePage = () => { const redirectUri = params.get("redirect_uri") ?? ""; const { isLoggedIn } = useUserContext(); - const { disableContinue } = useAppContext(); + const { disableContinue, domain } = useAppContext(); const { t } = useTranslation(); if (!isLoggedIn) { @@ -51,6 +51,30 @@ export const ContinuePage = () => { ); } + const regex = new RegExp(`^.*${escapeRegex(domain)}$`) + + if (!regex.test(uri.hostname)) { + return ( + + + {t("untrustedRedirectTitle")} + + }} + values={{ domain: domain }} + /> + + + + ) + } + if (disableContinue) { window.location.href = redirectUri; return ( @@ -79,6 +103,9 @@ export const ContinuePage = () => { + ); } diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index 987fff9..3738c3f 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -5,6 +5,7 @@ export const appContextSchema = z.object({ disableContinue: z.boolean(), title: z.string(), genericName: z.string(), + domain: z.string(), }); export type AppContextSchemaType = z.infer; diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 038230e..bef72fc 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -1,2 +1,3 @@ export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); export const isQueryValid = (value: string) => value.trim() !== "" && value !== "null"; +export const escapeRegex = (value: string) => value.replace(/[-\/\\^$.*+?()[\]{}|]/g, "\\$&"); \ No newline at end of file diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 99739f9..9e48764 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -446,6 +446,7 @@ func (h *Handlers) AppHandler(c *gin.Context) { DisableContinue: h.Config.DisableContinue, Title: h.Config.Title, GenericName: h.Config.GenericName, + Domain: h.Config.Domain, } // Return app context diff --git a/internal/types/api.go b/internal/types/api.go index 2d8df9d..e9307a6 100644 --- a/internal/types/api.go +++ b/internal/types/api.go @@ -46,6 +46,7 @@ type AppContext struct { DisableContinue bool `json:"disableContinue"` Title string `json:"title"` GenericName string `json:"genericName"` + Domain string `json:"domain"` } // Totp request is the request for the totp endpoint