From 4d9f7b17efc156bd1a01c4a13d5e69120c99781c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 13 Oct 2025 19:22:07 +0200 Subject: [PATCH 1/8] bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7624d270..89be08bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,7 +530,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "defguard-proxy" -version = "1.5.1" +version = "1.6.0" dependencies = [ "ammonia", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index d04ad4cb..002d3184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard-proxy" -version = "1.5.1" +version = "1.6.0" edition = "2021" license = "Apache-2.0" homepage = "https://github.com/DefGuard/proxy" From e38317c20baba646ce36d25ee55225d6a2c31638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 13 Oct 2025 19:56:59 +0200 Subject: [PATCH 2/8] pin pnpm version --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69770abe..69c7fda8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,7 +115,8 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 10 + # FIXME: temporarily pinned because of https://github.com/pnpm/pnpm/pull/9959 + version: 10.17 - name: Use Node.js uses: actions/setup-node@v4 From 0ea56b53a64c946b6e8795a606365ea654d330f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:48:23 +0200 Subject: [PATCH 3/8] add missing openid routes (#201) --- web/messages/en.json | 7 ++- web/src/routeTree.gen.ts | 63 +++++++++++++++++++ web/src/routes/openid/callback.tsx | 37 +++++++---- web/src/routes/openid/error.tsx | 22 +++++++ web/src/routes/openid/mfa/callback.tsx | 44 +++++++++++++ web/src/routes/openid/mfa/index.tsx | 58 +++++++++++++++++ .../PageProcessEnd/PageProcessEnd.tsx | 37 +++++------ .../components/PageProcessEnd/style.scss | 16 +++++ web/src/shared/consts.ts | 6 -- web/src/shared/defguard-ui | 2 +- web/src/shared/hooks/useOpenIdStore.tsx | 15 +++++ 11 files changed, 269 insertions(+), 38 deletions(-) create mode 100644 web/src/routes/openid/error.tsx create mode 100644 web/src/routes/openid/mfa/callback.tsx create mode 100644 web/src/routes/openid/mfa/index.tsx create mode 100644 web/src/shared/hooks/useOpenIdStore.tsx diff --git a/web/messages/en.json b/web/messages/en.json index 7fc0623a..e8caa20e 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -98,5 +98,10 @@ "client_setup_mobile_forgot": "If you forgot to install the mobile app, click one of the buttons bellow.", "client_setup_mobile_google": "Google Play", "client_setup_mobile_apple": "Apple Store", - "client_setup_footer_extra": "Once your Defguard client is configured, you can close this window." + "client_setup_footer_extra": "Once your Defguard client is configured, you can close this window.", + "openid_mfa_redirect_error_title": "Authentication Error", + "openid_mfa_redirect_error_message": "No token provided in the URL. Please ensure you have a valid token to proceed with OpenID authentication.", + "openid_mfa_redirect_error_missing_args": "Missing code or state in the callback's URL. The provider might not be configured correctly.", + "openid_mfa_complete_title": "Authentication Completed", + "openid_mfa_complete_subtitle": "You have been successfully authenticated. Please close this window and get back to the Defguard VPN Client" } diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 2dc0610c..0e0019e5 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -19,7 +19,10 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as PasswordIndexRouteImport } from './routes/password/index' import { Route as PasswordSentRouteImport } from './routes/password/sent' import { Route as PasswordFinishRouteImport } from './routes/password/finish' +import { Route as OpenidErrorRouteImport } from './routes/openid/error' import { Route as OpenidCallbackRouteImport } from './routes/openid/callback' +import { Route as OpenidMfaIndexRouteImport } from './routes/openid/mfa/index' +import { Route as OpenidMfaCallbackRouteImport } from './routes/openid/mfa/callback' const TestRoute = TestRouteImport.update({ id: '/test', @@ -71,11 +74,26 @@ const PasswordFinishRoute = PasswordFinishRouteImport.update({ path: '/password/finish', getParentRoute: () => rootRouteImport, } as any) +const OpenidErrorRoute = OpenidErrorRouteImport.update({ + id: '/openid/error', + path: '/openid/error', + getParentRoute: () => rootRouteImport, +} as any) const OpenidCallbackRoute = OpenidCallbackRouteImport.update({ id: '/openid/callback', path: '/openid/callback', getParentRoute: () => rootRouteImport, } as any) +const OpenidMfaIndexRoute = OpenidMfaIndexRouteImport.update({ + id: '/openid/mfa/', + path: '/openid/mfa/', + getParentRoute: () => rootRouteImport, +} as any) +const OpenidMfaCallbackRoute = OpenidMfaCallbackRouteImport.update({ + id: '/openid/mfa/callback', + path: '/openid/mfa/callback', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -86,9 +104,12 @@ export interface FileRoutesByFullPath { '/session-end': typeof SessionEndRoute '/test': typeof TestRoute '/openid/callback': typeof OpenidCallbackRoute + '/openid/error': typeof OpenidErrorRoute '/password/finish': typeof PasswordFinishRoute '/password/sent': typeof PasswordSentRoute '/password': typeof PasswordIndexRoute + '/openid/mfa/callback': typeof OpenidMfaCallbackRoute + '/openid/mfa': typeof OpenidMfaIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -99,9 +120,12 @@ export interface FileRoutesByTo { '/session-end': typeof SessionEndRoute '/test': typeof TestRoute '/openid/callback': typeof OpenidCallbackRoute + '/openid/error': typeof OpenidErrorRoute '/password/finish': typeof PasswordFinishRoute '/password/sent': typeof PasswordSentRoute '/password': typeof PasswordIndexRoute + '/openid/mfa/callback': typeof OpenidMfaCallbackRoute + '/openid/mfa': typeof OpenidMfaIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -113,9 +137,12 @@ export interface FileRoutesById { '/session-end': typeof SessionEndRoute '/test': typeof TestRoute '/openid/callback': typeof OpenidCallbackRoute + '/openid/error': typeof OpenidErrorRoute '/password/finish': typeof PasswordFinishRoute '/password/sent': typeof PasswordSentRoute '/password/': typeof PasswordIndexRoute + '/openid/mfa/callback': typeof OpenidMfaCallbackRoute + '/openid/mfa/': typeof OpenidMfaIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -128,9 +155,12 @@ export interface FileRouteTypes { | '/session-end' | '/test' | '/openid/callback' + | '/openid/error' | '/password/finish' | '/password/sent' | '/password' + | '/openid/mfa/callback' + | '/openid/mfa' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -141,9 +171,12 @@ export interface FileRouteTypes { | '/session-end' | '/test' | '/openid/callback' + | '/openid/error' | '/password/finish' | '/password/sent' | '/password' + | '/openid/mfa/callback' + | '/openid/mfa' id: | '__root__' | '/' @@ -154,9 +187,12 @@ export interface FileRouteTypes { | '/session-end' | '/test' | '/openid/callback' + | '/openid/error' | '/password/finish' | '/password/sent' | '/password/' + | '/openid/mfa/callback' + | '/openid/mfa/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -168,9 +204,12 @@ export interface RootRouteChildren { SessionEndRoute: typeof SessionEndRoute TestRoute: typeof TestRoute OpenidCallbackRoute: typeof OpenidCallbackRoute + OpenidErrorRoute: typeof OpenidErrorRoute PasswordFinishRoute: typeof PasswordFinishRoute PasswordSentRoute: typeof PasswordSentRoute PasswordIndexRoute: typeof PasswordIndexRoute + OpenidMfaCallbackRoute: typeof OpenidMfaCallbackRoute + OpenidMfaIndexRoute: typeof OpenidMfaIndexRoute } declare module '@tanstack/react-router' { @@ -245,6 +284,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PasswordFinishRouteImport parentRoute: typeof rootRouteImport } + '/openid/error': { + id: '/openid/error' + path: '/openid/error' + fullPath: '/openid/error' + preLoaderRoute: typeof OpenidErrorRouteImport + parentRoute: typeof rootRouteImport + } '/openid/callback': { id: '/openid/callback' path: '/openid/callback' @@ -252,6 +298,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OpenidCallbackRouteImport parentRoute: typeof rootRouteImport } + '/openid/mfa/': { + id: '/openid/mfa/' + path: '/openid/mfa' + fullPath: '/openid/mfa' + preLoaderRoute: typeof OpenidMfaIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/openid/mfa/callback': { + id: '/openid/mfa/callback' + path: '/openid/mfa/callback' + fullPath: '/openid/mfa/callback' + preLoaderRoute: typeof OpenidMfaCallbackRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -264,9 +324,12 @@ const rootRouteChildren: RootRouteChildren = { SessionEndRoute: SessionEndRoute, TestRoute: TestRoute, OpenidCallbackRoute: OpenidCallbackRoute, + OpenidErrorRoute: OpenidErrorRoute, PasswordFinishRoute: PasswordFinishRoute, PasswordSentRoute: PasswordSentRoute, PasswordIndexRoute: PasswordIndexRoute, + OpenidMfaCallbackRoute: OpenidMfaCallbackRoute, + OpenidMfaIndexRoute: OpenidMfaIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/web/src/routes/openid/callback.tsx b/web/src/routes/openid/callback.tsx index 2304ba0a..0e9ee9c2 100644 --- a/web/src/routes/openid/callback.tsx +++ b/web/src/routes/openid/callback.tsx @@ -12,20 +12,33 @@ const schema = z.object({ export const Route = createFileRoute('/openid/callback')({ component: RouteComponent, validateSearch: schema, + onError: () => { + throw redirect({ to: '/enrollment-start', replace: true }); + }, loaderDeps: ({ search }) => ({ search }), beforeLoad: async ({ search }) => { - const openIdResponse = await api.openId.enrollmentCallback.callbackFn({ - data: { - code: search.code, - state: search.state, - type: 'enrollment', - }, - }); - const enrollmentStartResponse = await api.enrollment.start.callbackFn({ - data: { - token: openIdResponse.data.token, - }, - }); + const openIdResponse = await api.openId.enrollmentCallback + .callbackFn({ + data: { + code: search.code, + state: search.state, + type: 'enrollment', + }, + }) + .catch((e) => { + console.error(e); + throw redirect({ to: '/enrollment-start', replace: true }); + }); + const enrollmentStartResponse = await api.enrollment.start + .callbackFn({ + data: { + token: openIdResponse.data.token, + }, + }) + .catch((e) => { + console.error(e); + throw redirect({ to: '/enrollment-start', replace: true }); + }); useEnrollmentStore.setState({ enrollmentData: enrollmentStartResponse.data, token: openIdResponse.data.token, diff --git a/web/src/routes/openid/error.tsx b/web/src/routes/openid/error.tsx new file mode 100644 index 00000000..255c3dcf --- /dev/null +++ b/web/src/routes/openid/error.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { m } from '../../paraglide/messages'; +import { PageProcessEnd } from '../../shared/components/PageProcessEnd/PageProcessEnd'; +import { useOpenidStore } from '../../shared/hooks/useOpenIdStore'; + +export const Route = createFileRoute('/openid/error')({ + component: RouteComponent, +}); + +function RouteComponent() { + const openIdError = useOpenidStore( + (s) => s.error ?? m.openid_mfa_redirect_error_message(), + ); + + return ( + + ); +} diff --git a/web/src/routes/openid/mfa/callback.tsx b/web/src/routes/openid/mfa/callback.tsx new file mode 100644 index 00000000..260d4a62 --- /dev/null +++ b/web/src/routes/openid/mfa/callback.tsx @@ -0,0 +1,44 @@ +import { createFileRoute, redirect } from '@tanstack/react-router'; +import z from 'zod'; +import { m } from '../../../paraglide/messages'; +import { api } from '../../../shared/api/api'; +import { PageProcessEnd } from '../../../shared/components/PageProcessEnd/PageProcessEnd'; +import { useOpenidStore } from '../../../shared/hooks/useOpenIdStore'; + +const searchSchema = z.object({ + code: z.string().trim().min(1), + state: z.string().trim().min(1), +}); + +export const Route = createFileRoute('/openid/mfa/callback')({ + validateSearch: searchSchema, + onError: () => { + useOpenidStore.setState({ error: m.openid_mfa_redirect_error_missing_args() }); + throw redirect({ to: '/openid/error', replace: true }); + }, + loaderDeps: ({ search }) => ({ search }), + loader: async ({ deps }) => { + try { + await api.openId.mfaCallback.callbackFn({ + data: { + code: deps.search.code, + state: deps.search.state, + type: 'mfa', + }, + }); + } catch (e) { + console.error(e); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + return ( + + ); +} diff --git a/web/src/routes/openid/mfa/index.tsx b/web/src/routes/openid/mfa/index.tsx new file mode 100644 index 00000000..4be95977 --- /dev/null +++ b/web/src/routes/openid/mfa/index.tsx @@ -0,0 +1,58 @@ +import { createFileRoute, redirect, useLoaderData } from '@tanstack/react-router'; +import { useEffect } from 'react'; +import z from 'zod'; +import { api } from '../../../shared/api/api'; +import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; + +const searchSchema = z.object({ + token: z.string().trim().min(1), +}); + +export const Route = createFileRoute('/openid/mfa/')({ + component: RouteComponent, + validateSearch: searchSchema, + onError: () => { + throw redirect({ + to: '/openid/error', + replace: true, + }); + }, + loaderDeps: ({ search }) => ({ search }), + loader: async ({ deps }) => { + const response = await api.openId.authInfo + .callbackFn({ + data: { + type: 'mfa', + state: deps.search.token, + }, + }) + .catch((e) => { + console.error(e); + throw redirect({ + to: '/openid/error', + replace: true, + }); + }); + if (!isPresent(response.data.url)) { + console.error('Missing URL in server response.'); + throw redirect({ + to: '/openid/error', + replace: true, + }); + } + return response.data.url as string; + }, +}); + +function RouteComponent() { + const loaderData = useLoaderData({ + from: '/openid/mfa/', + }); + + useEffect(() => { + if (loaderData) { + window.location.href = loaderData; + } + }, [loaderData]); + return null; +} diff --git a/web/src/shared/components/PageProcessEnd/PageProcessEnd.tsx b/web/src/shared/components/PageProcessEnd/PageProcessEnd.tsx index 9dd2a261..1af90789 100644 --- a/web/src/shared/components/PageProcessEnd/PageProcessEnd.tsx +++ b/web/src/shared/components/PageProcessEnd/PageProcessEnd.tsx @@ -27,24 +27,25 @@ export const PageProcessEnd = ({ }: Props) => { return ( - - - - - {title} - - - - {subtitle} - - {isPresent(linkText) && isPresent(link) && ( - <> - - -