fix: cookie issues in Electron (#4115)
This commit is contained in:
@@ -9,10 +9,11 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { signIn, useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { signInCloud } from '../../../utils/cloud-utils';
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
import { forgetPasswordButton } from './style.css';
|
import { forgetPasswordButton } from './style.css';
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
|||||||
const [passwordError, setPasswordError] = useState(false);
|
const [passwordError, setPasswordError] = useState(false);
|
||||||
|
|
||||||
const onSignIn = useCallback(async () => {
|
const onSignIn = useCallback(async () => {
|
||||||
const res = await signIn('credentials', {
|
const res = await signInCloud('credentials', {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -17,26 +17,30 @@ export const signInCloud: typeof signIn = async (provider, ...rest) => {
|
|||||||
'_target'
|
'_target'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else if (provider === 'email') {
|
} else {
|
||||||
const [options, ...tail] = rest;
|
const [options, ...tail] = rest;
|
||||||
|
const callbackUrl =
|
||||||
|
runtimeConfig.serverUrlPrefix +
|
||||||
|
(provider === 'email' ? '/open-app/oauth-jwt' : location.pathname);
|
||||||
return signIn(
|
return signIn(
|
||||||
provider,
|
provider,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
callbackUrl: buildCallbackUrl('/open-app/oauth-jwt'),
|
callbackUrl: buildCallbackUrl(callbackUrl),
|
||||||
},
|
},
|
||||||
...tail
|
...tail
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
throw new Error('Unsupported provider');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return signIn(provider, ...rest);
|
return signIn(provider, ...rest);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signOutCloud: typeof signOut = async (...args) => {
|
export const signOutCloud: typeof signOut = async options => {
|
||||||
return signOut(...args).then(result => {
|
return signOut({
|
||||||
|
...options,
|
||||||
|
callbackUrl: '/',
|
||||||
|
}).then(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
getCurrentStore().set(refreshRootMetadataAtom);
|
getCurrentStore().set(refreshRootMetadataAtom);
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import type { App } from 'electron';
|
import type { App } from 'electron';
|
||||||
|
|
||||||
import { buildType, isDev } from './config';
|
import { buildType, CLOUD_BASE_URL, isDev } from './config';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import {
|
import {
|
||||||
handleOpenUrlInHiddenWindow,
|
handleOpenUrlInHiddenWindow,
|
||||||
|
mainWindowOrigin,
|
||||||
restoreOrCreateWindow,
|
restoreOrCreateWindow,
|
||||||
setCookie,
|
setCookie,
|
||||||
} from './main-window';
|
} from './main-window';
|
||||||
@@ -70,24 +71,36 @@ async function handleOauthJwt(url: string) {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const token = urlObj.searchParams.get('token');
|
const token = urlObj.searchParams.get('token');
|
||||||
const mainOrigin = new URL(mainWindow.webContents.getURL()).origin;
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
logger.error('no token in url', url);
|
logger.error('no token in url', url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSecure = CLOUD_BASE_URL.startsWith('https://');
|
||||||
|
|
||||||
// set token to cookie
|
// set token to cookie
|
||||||
await setCookie({
|
await setCookie({
|
||||||
url: mainOrigin,
|
url: CLOUD_BASE_URL,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
value: token,
|
value: token,
|
||||||
name: 'next-auth.session-token',
|
secure: true,
|
||||||
|
name: isSecure
|
||||||
|
? '__Secure-next-auth.session-token'
|
||||||
|
: 'next-auth.session-token',
|
||||||
|
expirationDate: Math.floor(Date.now() / 1000 + 3600 * 24 * 7),
|
||||||
|
});
|
||||||
|
|
||||||
|
// force reset next-auth.callback-url
|
||||||
|
await setCookie({
|
||||||
|
url: CLOUD_BASE_URL,
|
||||||
|
httpOnly: true,
|
||||||
|
name: 'next-auth.callback-url',
|
||||||
});
|
});
|
||||||
|
|
||||||
// hacks to refresh auth state in the main window
|
// hacks to refresh auth state in the main window
|
||||||
const window = await handleOpenUrlInHiddenWindow(
|
const window = await handleOpenUrlInHiddenWindow(
|
||||||
mainOrigin + '/auth/signIn'
|
mainWindowOrigin + '/auth/signIn'
|
||||||
);
|
);
|
||||||
uiSubjects.onFinishLogin.next({
|
uiSubjects.onFinishLogin.next({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const IS_DEV: boolean =
|
|||||||
|
|
||||||
const DEV_TOOL = process.env.DEV_TOOL === 'true';
|
const DEV_TOOL = process.env.DEV_TOOL === 'true';
|
||||||
|
|
||||||
|
export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.';
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
logger.info('create window');
|
logger.info('create window');
|
||||||
const mainWindowState = electronWindowState({
|
const mainWindowState = electronWindowState({
|
||||||
@@ -114,7 +116,7 @@ async function createWindow() {
|
|||||||
/**
|
/**
|
||||||
* URL for main window.
|
* URL for main window.
|
||||||
*/
|
*/
|
||||||
const pageUrl = process.env.DEV_SERVER_URL || 'file://.'; // see protocol.ts
|
const pageUrl = mainWindowOrigin; // see protocol.ts
|
||||||
|
|
||||||
logger.info('loading page at', pageUrl);
|
logger.info('loading page at', pageUrl);
|
||||||
|
|
||||||
@@ -126,35 +128,30 @@ async function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// singleton
|
// singleton
|
||||||
let browserWindow: BrowserWindow | undefined;
|
let browserWindow$: Promise<BrowserWindow> | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore existing BrowserWindow or Create new BrowserWindow
|
* Restore existing BrowserWindow or Create new BrowserWindow
|
||||||
*/
|
*/
|
||||||
export async function restoreOrCreateWindow() {
|
export async function restoreOrCreateWindow() {
|
||||||
if (!browserWindow || browserWindow.isDestroyed()) {
|
if (!browserWindow$ || (await browserWindow$.then(w => w.isDestroyed()))) {
|
||||||
browserWindow = await createWindow();
|
browserWindow$ = createWindow();
|
||||||
}
|
}
|
||||||
|
const mainWindow = await browserWindow$;
|
||||||
|
|
||||||
if (browserWindow.isMinimized()) {
|
if (mainWindow.isMinimized()) {
|
||||||
browserWindow.restore();
|
mainWindow.restore();
|
||||||
logger.info('restore main window');
|
logger.info('restore main window');
|
||||||
}
|
}
|
||||||
|
return mainWindow;
|
||||||
return browserWindow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleOpenUrlInHiddenWindow(url: string) {
|
export async function handleOpenUrlInHiddenWindow(url: string) {
|
||||||
const mainExposedMeta = getExposedMeta();
|
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 600,
|
height: 600,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
additionalArguments: [
|
|
||||||
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
|
|
||||||
// popup window does not need helper process, right?
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
show: false,
|
show: false,
|
||||||
});
|
});
|
||||||
@@ -169,10 +166,6 @@ export async function handleOpenUrlInHiddenWindow(url: string) {
|
|||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reloadApp() {
|
|
||||||
browserWindow?.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setCookie(cookie: CookiesSetDetails): Promise<void>;
|
export async function setCookie(cookie: CookiesSetDetails): Promise<void>;
|
||||||
export async function setCookie(origin: string, cookie: string): Promise<void>;
|
export async function setCookie(origin: string, cookie: string): Promise<void>;
|
||||||
|
|
||||||
@@ -186,9 +179,20 @@ export async function setCookie(
|
|||||||
? parseCookie(arg0, arg1)
|
? parseCookie(arg0, arg1)
|
||||||
: arg0;
|
: arg0;
|
||||||
|
|
||||||
|
logger.info('setting cookie to main window', details);
|
||||||
|
|
||||||
if (typeof details !== 'object') {
|
if (typeof details !== 'object') {
|
||||||
throw new Error('invalid cookie details');
|
throw new Error('invalid cookie details');
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.webContents.session.cookies.set(details);
|
await window.webContents.session.cookies.set(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCookie(url?: string, name?: string) {
|
||||||
|
const window = await restoreOrCreateWindow();
|
||||||
|
const cookies = await window.webContents.session.cookies.get({
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { net, protocol, session } from 'electron';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { CLOUD_BASE_URL } from './config';
|
import { CLOUD_BASE_URL } from './config';
|
||||||
|
import { logger } from './logger';
|
||||||
|
import { getCookie } from './main-window';
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{
|
{
|
||||||
@@ -70,9 +72,49 @@ export function registerProtocol() {
|
|||||||
'DELETE',
|
'DELETE',
|
||||||
'OPTIONS',
|
'OPTIONS',
|
||||||
];
|
];
|
||||||
|
// replace SameSite=Lax with SameSite=None
|
||||||
|
const originalCookie =
|
||||||
|
responseHeaders['set-cookie'] || responseHeaders['Set-Cookie'];
|
||||||
|
|
||||||
|
if (originalCookie) {
|
||||||
|
delete responseHeaders['set-cookie'];
|
||||||
|
delete responseHeaders['Set-Cookie'];
|
||||||
|
responseHeaders['Set-Cookie'] = originalCookie.map(cookie => {
|
||||||
|
let newCookie = cookie.replace(/SameSite=Lax/gi, 'SameSite=None');
|
||||||
|
|
||||||
|
// if the cookie is not secure, set it to secure
|
||||||
|
if (!newCookie.includes('Secure')) {
|
||||||
|
newCookie = newCookie + '; Secure';
|
||||||
|
}
|
||||||
|
return newCookie;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback({ responseHeaders });
|
callback({ responseHeaders });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||||
|
(async () => {
|
||||||
|
const url = new URL(details.url);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
// if sending request to the cloud, attach the session cookie
|
||||||
|
if (isNetworkResource(pathname)) {
|
||||||
|
const cookie = await getCookie(CLOUD_BASE_URL);
|
||||||
|
const cookieString = cookie.map(c => `${c.name}=${c.value}`).join('; ');
|
||||||
|
details.requestHeaders['cookie'] = cookieString;
|
||||||
|
}
|
||||||
|
callback({
|
||||||
|
cancel: false,
|
||||||
|
requestHeaders: details.requestHeaders,
|
||||||
|
});
|
||||||
|
})().catch(e => {
|
||||||
|
logger.error('failed to attach cookie', e);
|
||||||
|
callback({
|
||||||
|
cancel: false,
|
||||||
|
requestHeaders: details.requestHeaders,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
|||||||
adapter: prismaAdapter,
|
adapter: prismaAdapter,
|
||||||
debug: !config.node.prod,
|
debug: !config.node.prod,
|
||||||
session: {
|
session: {
|
||||||
strategy: config.node.prod ? 'database' : 'jwt',
|
strategy: 'jwt',
|
||||||
},
|
},
|
||||||
// @ts-expect-error Third part library type mismatch
|
// @ts-expect-error Third part library type mismatch
|
||||||
logger: console,
|
logger: console,
|
||||||
|
|||||||
Reference in New Issue
Block a user