diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index df4976b85..000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Without running core on windows will result in stack overflow -[target.x86_64-pc-windows-msvc] -rustflags = ["-C", "link-args=/STACK:8388608"] diff --git a/.gitmodules b/.gitmodules index a5df1c851..561704131 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = ../proto.git [submodule "web/src/shared/defguard-ui"] path = web/src/shared/defguard-ui - url = ../ui.git + url = git@github.com:DefGuard/ui.git diff --git a/e2e/tests/externalopenid.spec.ts b/e2e/tests/externalopenid.spec.ts index 00d74fda6..4a1497b0f 100644 --- a/e2e/tests/externalopenid.spec.ts +++ b/e2e/tests/externalopenid.spec.ts @@ -1,7 +1,8 @@ import { expect, test } from '@playwright/test'; -import { defaultUserAdmin, routes, testsConfig, testUserTemplate } from '../config'; +import { defaultUserAdmin, testsConfig, testUserTemplate } from '../config'; import { NetworkForm, OpenIdClient, User } from '../types'; +// import { apiEnrollmentActivateUser, apiEnrollmentStart } from '../utils/api/enrollment'; import { apiCreateUser } from '../utils/api/users'; import { loginBasic } from '../utils/controllers/login'; import { logout } from '../utils/controllers/logout'; @@ -12,7 +13,7 @@ import { createRegularLocation } from '../utils/controllers/vpn/createNetwork'; import { dockerRestart } from '../utils/docker'; import { waitForBase } from '../utils/waitForBase'; import { waitForPromise } from '../utils/waitForPromise'; -import { waitForRoute } from '../utils/waitForRoute'; +// import { waitForRoute } from '../utils/waitForRoute'; test.describe('External OIDC.', () => { const testUser: User = { ...testUserTemplate, username: 'test' }; @@ -51,47 +52,48 @@ test.describe('External OIDC.', () => { await context.close(); }); - test('Login through external oidc.', async ({ page }) => { - expect(client.clientID).toBeDefined(); - expect(client.clientSecret).toBeDefined(); - await waitForBase(page); - const oidcLoginButton = await page.locator('.oidc-button'); - expect(oidcLoginButton).not.toBeNull(); - expect(await oidcLoginButton.textContent()).toBe(`Sign in with ${client.name}`); - await oidcLoginButton.click(); - await page.getByTestId('login-form-username').fill(testUser.username); - await page.getByTestId('login-form-password').fill(testUser.password); - await page.getByTestId('login-form-submit').click(); - await page.getByTestId('openid-allow').click(); - await waitForRoute(page, routes.me); - const authorizedApps = await page - .getByTestId('authorized-apps') - .locator('div') - .textContent(); - expect(authorizedApps).toContain(client.name); - }); + // TODO: Finish when https://github.com/DefGuard/defguard/issues/1817 is resolved + // test('Login through external oidc.', async ({ page }) => { + // expect(client.clientID).toBeDefined(); + // expect(client.clientSecret).toBeDefined(); + // await waitForBase(page); + // const oidcLoginButton = await page.locator('.oidc-button'); + // expect(oidcLoginButton).not.toBeNull(); + // expect(await oidcLoginButton.textContent()).toBe(`Sign in with ${client.name}`); + // await oidcLoginButton.click(); + // await page.getByTestId('login-form-username').fill(testUser.username); + // await page.getByTestId('login-form-password').fill(testUser.password); + // await page.getByTestId('login-form-submit').click(); + // await page.getByTestId('openid-allow').click(); + // await waitForRoute(page, routes.me); + // const authorizedApps = await page + // .getByTestId('authorized-apps') + // .locator('div') + // .textContent(); + // expect(authorizedApps).toContain(client.name); + // }); - test('Complete enrollment through external OIDC', async ({ page }) => { + test('Sign in with external SSO', async ({ page }) => { await waitForBase(page); await page.goto(testsConfig.ENROLLMENT_URL); await waitForPromise(2000); - await page.getByTestId('select-enrollment').click(); - await page.getByTestId('login-oidc').click(); - await page.getByTestId('login-form-username').fill(testUser.username); - await page.getByTestId('login-form-password').fill(testUser.password); - await page.getByTestId('login-form-submit').click(); - await page.getByTestId('openid-allow').click(); - const instanceUrlBoxText = await page - .locator('div.copy-field div.list-cell-text ') - .first() - .textContent(); - expect(instanceUrlBoxText).toBe(testsConfig.ENROLLMENT_URL + '/'); + await page.getByTestId('start-enrollment').click(); + await page.locator('.oidc-button-link').click(); + await page.getByTestId('field-username').fill(defaultUserAdmin.username); + await page.getByTestId('field-password').fill(defaultUserAdmin.password); + await page.getByTestId('sign-in').click(); + await page.getByTestId('accept-openid').click(); + await page.getByTestId('page-nav-next').click(); + await page.getByTestId('modal-confirm-download-submit').click(); + + const setup_desktop = await page.locator('#setup-desktop'); + await setup_desktop.locator('.fold-button').click(); - const instanceTokenBoxText = await page - .locator('div.copy-field div.list-cell-text ') - .nth(1) + const token = await page + .locator('.copy-field') + .filter({ hasText: 'Token' }) + .locator('.track p') .textContent(); - expect(instanceTokenBoxText).toBeDefined(); - expect(instanceTokenBoxText?.length).toBeGreaterThan(1); + expect(token).toBeDefined(); }); }); diff --git a/e2e/tests/externalopenidmfa.spec.ts b/e2e/tests/externalopenidmfa.spec.ts new file mode 100644 index 000000000..cb45e0e49 --- /dev/null +++ b/e2e/tests/externalopenidmfa.spec.ts @@ -0,0 +1,100 @@ +import { test } from '@playwright/test'; + +import { defaultUserAdmin, testsConfig, testUserTemplate } from '../config'; +import { NetworkForm, OpenIdClient, User } from '../types'; +import { apiCreateUser } from '../utils/api/users'; +import { loginBasic } from '../utils/controllers/login'; +import { logout } from '../utils/controllers/logout'; +import { copyOpenIdClientIdAndSecret } from '../utils/controllers/openid/copyClientId'; +import { createExternalProvider } from '../utils/controllers/openid/createExternalProvider'; +import { CreateOpenIdClient } from '../utils/controllers/openid/createOpenIdClient'; +import { createRegularLocation } from '../utils/controllers/vpn/createNetwork'; +import { dockerRestart } from '../utils/docker'; + +test.describe('External OIDC.', () => { + const testUser: User = { ...testUserTemplate, username: 'test' }; + + const client: OpenIdClient = { + name: 'test 01', + redirectURL: [`${testsConfig.ENROLLMENT_URL}/openid/mfa/callback`], + scopes: ['openid', 'profile', 'email'], + }; + + const testNetwork: NetworkForm = { + name: 'test network', + address: '10.10.10.1/24', + endpoint: '127.0.0.1', + port: '5055', + location_mfa_mode: 'external', + }; + + test.beforeEach(async ({ browser }) => { + dockerRestart(); + await CreateOpenIdClient(browser, client); + [client.clientID, client.clientSecret] = await copyOpenIdClientIdAndSecret( + browser, + client.name, + ); + const context = await browser.newContext(); + const page = await context.newPage(); + await createExternalProvider(browser, client); + await loginBasic(page, defaultUserAdmin); + await apiCreateUser(page, testUser); + await logout(page); + await createRegularLocation(browser, testNetwork); + await context.close(); + }); + + // TODO: Finish when https://github.com/DefGuard/defguard/issues/1817 is resolved + // test('Complete client MFA through external OpenID', async ({ page, browser }) => { + // await waitForBase(page); + // const mfaStartUrl = `${testsConfig.ENROLLMENT_URL}/api/v1/client-mfa/start`; + // await createDevice(browser, testUser, { + // name: 'test', + // }); + // await loginBasic(page, testUser); + // const testUserProfile = await apiGetUserProfile(page, testUser.username); + // expect(testUserProfile.devices.length).toBe(1); + // const createdDevice = testUserProfile.devices[0]; + // const pubkey = createdDevice.wireguard_pubkey; + // const data = { + // method: 2, + // pubkey: pubkey, + // location_id: 1, + // }; + // const response = await page.request.post(mfaStartUrl, { + // data: data, + // }); + // expect(response.ok()).toBeTruthy(); + // const { token } = await response.json(); + // expect(token).toBeDefined(); + // expect(token.length).toBeGreaterThan(0); + + // const preconditionResponse = await page.request.post( + // testsConfig.ENROLLMENT_URL + '/api/v1/client-mfa/finish', + // { + // data: { + // token: token, + // }, + // }, + // ); + // expect(preconditionResponse.status()).toBe(428); + + // const url = testsConfig.ENROLLMENT_URL + '/openid/mfa' + `?token=${token}`; + // await page.goto(url); + // await waitForPromise(2000); + // await page.getByTestId('openid-allow').click(); + // await waitForPromise(2000); + + // const finish = testsConfig.ENROLLMENT_URL + '/api/v1/client-mfa/finish'; + // const finishResponse = await page.request.post(finish, { + // data: { + // token: token, + // }, + // }); + // expect(finishResponse.ok()).toBeTruthy(); + // const finishData = await finishResponse.json(); + // expect(finishData.preshared_key).toBeDefined(); + // expect(finishData.preshared_key.length).toBeGreaterThan(0); + // }); +}); diff --git a/e2e/utils/controllers/mfa/enableTOTP.ts b/e2e/utils/controllers/mfa/enableTOTP.ts index 0c84bcd61..691670e54 100644 --- a/e2e/utils/controllers/mfa/enableTOTP.ts +++ b/e2e/utils/controllers/mfa/enableTOTP.ts @@ -1,4 +1,5 @@ import { Browser } from 'playwright'; +import { expect } from 'playwright/test'; import { TOTP } from 'totp-generator'; import { routes } from '../../../config'; @@ -40,6 +41,8 @@ export const enableTOTP = async ( await page.getByTestId('confirm-code-save').click(); await page.getByTestId('finish-recovery-codes').click(); + expect(totpSecret).toBeDefined(); + expect(recovery).toBeDefined(); await context.close(); return { diff --git a/proto b/proto index c48340f72..883487df6 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit c48340f72b9de3a69cf71318c75ff1361ebd7897 +Subproject commit 883487df67d90fd14fae900737cd8b5ea6c10de3 diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 788f93983..ad1f64c47 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -272,6 +272,14 @@ packages: '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 '@babel/traverse@7.28.6': resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index adaabe675..f65ffc0af 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as SnackbarRouteImport } from './routes/snackbar' import { Route as PlaygroundRouteImport } from './routes/playground' import { Route as ConsentRouteImport } from './routes/consent' import { Route as AuthRouteImport } from './routes/auth' +import { Route as AppLoaderRouteImport } from './routes/app-loader' import { Route as AuthorizedRouteImport } from './routes/_authorized' import { Route as R404RouteImport } from './routes/404' import { Route as AuthIndexRouteImport } from './routes/auth/index' @@ -71,6 +72,11 @@ const AuthRoute = AuthRouteImport.update({ path: '/auth', getParentRoute: () => rootRouteImport, } as any) +const AppLoaderRoute = AppLoaderRouteImport.update({ + id: '/app-loader', + path: '/app-loader', + getParentRoute: () => rootRouteImport, +} as any) const AuthorizedRoute = AuthorizedRouteImport.update({ id: '/_authorized', getParentRoute: () => rootRouteImport, @@ -279,6 +285,7 @@ const AuthorizedDefaultLocationsLocationIdEditRoute = export interface FileRoutesByFullPath { '/404': typeof R404Route + '/app-loader': typeof AppLoaderRoute '/auth': typeof AuthRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute @@ -320,6 +327,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/404': typeof R404Route + '/app-loader': typeof AppLoaderRoute '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute '/snackbar': typeof SnackbarRoute @@ -362,6 +370,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/404': typeof R404Route '/_authorized': typeof AuthorizedRouteWithChildren + '/app-loader': typeof AppLoaderRoute '/auth': typeof AuthRouteWithChildren '/consent': typeof ConsentRoute '/playground': typeof PlaygroundRoute @@ -406,6 +415,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/404' + | '/app-loader' | '/auth' | '/consent' | '/playground' @@ -447,6 +457,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/404' + | '/app-loader' | '/consent' | '/playground' | '/snackbar' @@ -488,6 +499,7 @@ export interface FileRouteTypes { | '__root__' | '/404' | '/_authorized' + | '/app-loader' | '/auth' | '/consent' | '/playground' @@ -532,6 +544,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { R404Route: typeof R404Route AuthorizedRoute: typeof AuthorizedRouteWithChildren + AppLoaderRoute: typeof AppLoaderRoute AuthRoute: typeof AuthRouteWithChildren ConsentRoute: typeof ConsentRoute PlaygroundRoute: typeof PlaygroundRoute @@ -568,6 +581,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthRouteImport parentRoute: typeof rootRouteImport } + '/app-loader': { + id: '/app-loader' + path: '/app-loader' + fullPath: '/app-loader' + preLoaderRoute: typeof AppLoaderRouteImport + parentRoute: typeof rootRouteImport + } '/_authorized': { id: '/_authorized' path: '' @@ -946,6 +966,7 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) const rootRouteChildren: RootRouteChildren = { R404Route: R404Route, AuthorizedRoute: AuthorizedRouteWithChildren, + AppLoaderRoute: AppLoaderRoute, AuthRoute: AuthRouteWithChildren, ConsentRoute: ConsentRoute, PlaygroundRoute: PlaygroundRoute, diff --git a/web/src/routes/app-loader.tsx b/web/src/routes/app-loader.tsx new file mode 100644 index 000000000..48c77f3fe --- /dev/null +++ b/web/src/routes/app-loader.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { AppLoaderPage } from '../pages/AppLoaderPage/AppLoaderPage'; + +export const Route = createFileRoute('/app-loader')({ + component: AppLoaderPage, +}); diff --git a/web/src/shared/hooks/modalControls/modalsSubjects.ts b/web/src/shared/hooks/modalControls/modalsSubjects.ts index 14e507327..3cb338e36 100644 --- a/web/src/shared/hooks/modalControls/modalsSubjects.ts +++ b/web/src/shared/hooks/modalControls/modalsSubjects.ts @@ -36,7 +36,6 @@ type PayloadOf = } ? D : undefined; - export function openModal( name: N, ...args: PayloadOf extends undefined ? [] : [PayloadOf]