refactor: auto retry the auth flow

This commit is contained in:
Kevin Yue
2023-08-31 21:18:30 +08:00
parent 3098d1170f
commit 3674a28dee
12 changed files with 135 additions and 33 deletions

View File

@@ -1,13 +1,19 @@
import { atom } from "jotai";
import authService from "../services/authService";
import portalService, { Prelogin } from "../services/portalService";
import { loginPortalAtom } from "./loginPortal";
import portalService, {
Prelogin,
SamlPrelogin,
} from "../services/portalService";
import logger from "../utils/logger";
import { AbnormalPortalConfigError, loginPortalAtom } from "./loginPortal";
import { notifyErrorAtom } from "./notification";
import { launchPasswordLoginAtom } from "./passwordLogin";
import { currentPortalDataAtom, portalAddressAtom } from "./portal";
import { launchSamlLoginAtom, retrySamlLoginAtom } from "./samlLogin";
import { isProcessingAtom, statusAtom } from "./status";
const MAX_ATTEMPTS = 10;
/**
* Connect to the portal, workflow:
* 1. Portal prelogin to get the prelogin data
@@ -31,10 +37,10 @@ export const connectPortalAtom = atom(
try {
set(statusAtom, "prelogin");
const prelogin = await portalService.prelogin(portal);
let prelogin = await portalService.prelogin(portal);
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
logger.info("Operation cancelled");
return;
}
@@ -44,7 +50,31 @@ export const connectPortalAtom = atom(
} catch {
// Otherwise, login with SAML or the password
if (prelogin.isSamlAuth) {
await set(launchSamlLoginAtom, prelogin);
let attemptCount = 0;
while (true) {
try {
attemptCount++;
logger.info(
`(${attemptCount}/${MAX_ATTEMPTS}) Launching SAML login...`
);
await set(launchSamlLoginAtom, prelogin as SamlPrelogin);
break;
} catch (err) {
if (attemptCount >= MAX_ATTEMPTS) {
throw err;
}
if (err instanceof AbnormalPortalConfigError) {
logger.info(
`Got abnormal portal config: ${err.message}, retrying...`
);
prelogin = await portalService.prelogin(portal);
continue;
} else {
throw err;
}
}
}
} else {
set(launchPasswordLoginAtom, prelogin);
}

View File

@@ -1,5 +1,6 @@
import { atom } from "jotai";
import gatewayService from "../services/gatewayService";
import logger from "../utils/logger";
import { isProcessingAtom, statusAtom } from "./status";
import { connectVpnAtom } from "./vpn";
@@ -19,6 +20,7 @@ export const loginGatewayAtom = atom(
set(statusAtom, "gateway-login");
let token: string;
try {
logger.info(`Logging in to gateway ${gateway}...`);
token = await gatewayService.login(gateway, credential);
} catch (err) {
throw new Error("Failed to login to gateway");
@@ -26,7 +28,7 @@ export const loginGatewayAtom = atom(
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
logger.info("Operation cancelled");
return;
}

View File

@@ -4,11 +4,23 @@ import portalService, {
PortalCredential,
Prelogin,
} from "../services/portalService";
import logger from "../utils/logger";
import { redact } from "../utils/redact";
import { selectedGatewayAtom } from "./gateway";
import { loginGatewayAtom } from "./loginGateway";
import { portalAddressAtom, updatePortalDataAtom } from "./portal";
import { isProcessingAtom, statusAtom } from "./status";
/**
* The error thrown when the portal config is abnormal, can back to normal after retrying
*/
export class AbnormalPortalConfigError extends Error {
constructor(message: string) {
super(message);
this.name = "AbnormalPortalConfigError";
}
}
// Indicates whether the portal config is being fetched
// This is mainly used to show the loading indicator in the password login form
const portalConfigLoadingAtom = atom(false);
@@ -42,23 +54,30 @@ export const loginPortalAtom = atom(
try {
portalConfig = await portalService.fetchConfig(portalAddress, credential);
configFetched?.();
} catch (err) {
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
logger.info("Operation cancelled");
return;
}
throw err;
} finally {
set(portalConfigLoadingAtom, false);
}
const isProcessing = get(isProcessingAtom);
if (!isProcessing) {
console.info("Request cancelled");
logger.info("Operation cancelled");
return;
}
const { gateways, userAuthCookie, prelogonUserAuthCookie } = portalConfig;
if (!gateways.length) {
throw new Error("No gateway found");
throw new AbnormalPortalConfigError("No gateway found");
}
if (userAuthCookie === "empty" || prelogonUserAuthCookie === "empty") {
throw new Error("Got empty user auth cookie");
throw new AbnormalPortalConfigError("Empty user auth cookie");
}
// Here, we have got the portal config successfully, refresh the cached portal data
@@ -87,6 +106,8 @@ export const loginPortalAtom = atom(
preferredGateway: previousSelectedGateway,
});
logger.info(`Found the preferred gateway: ${name} (${redact(address)})`);
// Log in to the gateway
await set(loginGatewayAtom, address, {
user: credential.user,

View File

@@ -1,10 +1,11 @@
import { atom } from "jotai";
import authService, { AuthData } from "../services/authService";
import portalService, { SamlPrelogin } from "../services/portalService";
import logger from "../utils/logger";
import { redact } from "../utils/redact";
import { loginPortalAtom } from "./loginPortal";
import { clearCookiesAtom, portalAddressAtom } from "./portal";
import { statusAtom } from "./status";
import { unwrap } from "./unwrap";
export const launchSamlLoginAtom = atom(
null,
@@ -38,7 +39,11 @@ export const launchSamlLoginAtom = atom(
"prelogin-cookie": authData.prelogin_cookie,
"portal-userauthcookie": authData.portal_userauthcookie,
};
logger.info(
`SAML login succeeded, prelogin-cookie: ${redact(
authData.prelogin_cookie
)}, portal-userauthcookie: ${redact(authData.portal_userauthcookie)}`
);
await set(loginPortalAtom, credential, prelogin);
}
);

View File

@@ -1,5 +1,7 @@
import { atom } from "jotai";
import vpnService from "../services/vpnService";
import logger from "../utils/logger";
import { redact } from "../utils/redact";
import { notifyErrorAtom } from "./notification";
import { statusAtom } from "./status";
@@ -8,6 +10,11 @@ export const connectVpnAtom = atom(
async (_get, set, vpnAddress: string, token: string) => {
try {
set(statusAtom, "connecting");
logger.info(
`Connecting to VPN ${redact(vpnAddress)} with token ${redact(
token
)} ...`
);
await vpnService.connect(vpnAddress, token);
set(statusAtom, "connected");
} catch (err) {
@@ -20,6 +27,7 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const disconnectVpnAtom = atom(null, async (get, set) => {
try {
set(statusAtom, "disconnecting");
logger.info("Disconnecting from VPN...");
await vpnService.disconnect();
// Sleep a short time, so that the client can receive the service's disconnected event.
await sleep(100);