mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
Merge pull request #788 from ZacharyZcR/feat/system-browser-oidc-auth
feat: implement RFC 8252 system browser OIDC authentication for desktop
This commit is contained in:
@@ -1088,6 +1088,42 @@ ipcMain.handle("get-embedded-server-status", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// OIDC System Browser Authentication (RFC 8252)
|
||||||
|
ipcMain.handle("oidc-system-browser-auth", async (_event, authUrl, callbackPort) => {
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, `http://localhost:${callbackPort}`);
|
||||||
|
if (url.pathname === "/oidc-callback") {
|
||||||
|
const success = url.searchParams.get("success");
|
||||||
|
const error = url.searchParams.get("error");
|
||||||
|
const token = url.searchParams.get("token");
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html" });
|
||||||
|
res.end(`<html><body><h2>${success === "true" ? "Authentication successful!" : "Authentication failed."}</h2><p>You can close this tab and return to Termix.</p><script>window.close()</script></body></html>`);
|
||||||
|
|
||||||
|
server.close();
|
||||||
|
if (success === "true") {
|
||||||
|
resolve({ success: true, token });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: error || "Authentication failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(callbackPort, "127.0.0.1", () => {
|
||||||
|
shell.openExternal(authUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 5 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
server.close();
|
||||||
|
reject(new Error("OIDC authentication timed out"));
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-server-config", () => {
|
ipcMain.handle("get-server-config", () => {
|
||||||
try {
|
try {
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
timeoutMs,
|
timeoutMs,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
oidcSystemBrowserAuth: (authUrl, callbackPort) =>
|
||||||
|
ipcRenderer.invoke("oidc-system-browser-auth", authUrl, callbackPort),
|
||||||
|
|
||||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -819,7 +819,7 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get("/oidc/authorize", async (req, res) => {
|
router.get("/oidc/authorize", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { rememberMe } = req.query;
|
const { rememberMe, desktopCallbackPort } = req.query;
|
||||||
const origin = getRequestOriginWithForceHTTPS(req);
|
const origin = getRequestOriginWithForceHTTPS(req);
|
||||||
const backendCallbackUri = `${origin}/users/oidc/callback`;
|
const backendCallbackUri = `${origin}/users/oidc/callback`;
|
||||||
|
|
||||||
@@ -842,7 +842,9 @@ router.get("/oidc/authorize", async (req, res) => {
|
|||||||
|
|
||||||
const referer = req.get("Referer");
|
const referer = req.get("Referer");
|
||||||
let frontendOrigin;
|
let frontendOrigin;
|
||||||
if (referer) {
|
if (desktopCallbackPort) {
|
||||||
|
frontendOrigin = `http://127.0.0.1:${desktopCallbackPort}/oidc-callback`;
|
||||||
|
} else if (referer) {
|
||||||
const refererUrl = new URL(referer);
|
const refererUrl = new URL(referer);
|
||||||
frontendOrigin = `${refererUrl.protocol}//${refererUrl.host}`;
|
frontendOrigin = `${refererUrl.protocol}//${refererUrl.host}`;
|
||||||
} else {
|
} else {
|
||||||
@@ -1365,6 +1367,8 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
const redirectUrl = new URL(frontendOrigin);
|
const redirectUrl = new URL(frontendOrigin);
|
||||||
redirectUrl.searchParams.set("success", "true");
|
redirectUrl.searchParams.set("success", "true");
|
||||||
|
|
||||||
|
const isDesktopCallback = frontendOrigin.startsWith("http://127.0.0.1:");
|
||||||
|
|
||||||
const maxAge =
|
const maxAge =
|
||||||
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
|
deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
|
||||||
? 30 * 24 * 60 * 60 * 1000
|
? 30 * 24 * 60 * 60 * 1000
|
||||||
@@ -1374,6 +1378,11 @@ router.get("/oidc/callback", async (req, res) => {
|
|||||||
|
|
||||||
res.clearCookie("jwt", authManager.getClearCookieOptions(req));
|
res.clearCookie("jwt", authManager.getClearCookieOptions(req));
|
||||||
|
|
||||||
|
if (isDesktopCallback) {
|
||||||
|
redirectUrl.searchParams.set("token", token);
|
||||||
|
return res.redirect(redirectUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
|
.cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
|
||||||
.redirect(redirectUrl.toString());
|
.redirect(redirectUrl.toString());
|
||||||
|
|||||||
@@ -646,6 +646,22 @@ export function Auth({ onLogin }: AuthProps) {
|
|||||||
async function handleOIDCLogin() {
|
async function handleOIDCLogin() {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
|
if (isElectron()) {
|
||||||
|
const electronAPI = (window as unknown as { electronAPI?: { oidcSystemBrowserAuth?: (authUrl: string, port: number) => Promise<{ success: boolean; token?: string; error?: string }> } }).electronAPI;
|
||||||
|
if (electronAPI?.oidcSystemBrowserAuth) {
|
||||||
|
const callbackPort = 17832 + Math.floor(Math.random() * 100);
|
||||||
|
const authResponse = await getOIDCAuthorizeUrl(rememberMe, callbackPort);
|
||||||
|
const { auth_url: authUrl } = authResponse;
|
||||||
|
if (!authUrl) throw new Error(t("errors.invalidAuthUrl"));
|
||||||
|
const result = await electronAPI.oidcSystemBrowserAuth(authUrl, callbackPort);
|
||||||
|
if (result.success && result.token) {
|
||||||
|
localStorage.setItem("jwt_token", result.token);
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(result.error || "Authentication failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
const authResponse = await getOIDCAuthorizeUrl(rememberMe);
|
const authResponse = await getOIDCAuthorizeUrl(rememberMe);
|
||||||
const { auth_url: authUrl } = authResponse;
|
const { auth_url: authUrl } = authResponse;
|
||||||
if (!authUrl || authUrl === "undefined")
|
if (!authUrl || authUrl === "undefined")
|
||||||
|
|||||||
@@ -3121,10 +3121,11 @@ export async function changePassword(oldPassword: string, newPassword: string) {
|
|||||||
|
|
||||||
export async function getOIDCAuthorizeUrl(
|
export async function getOIDCAuthorizeUrl(
|
||||||
rememberMe = false,
|
rememberMe = false,
|
||||||
|
desktopCallbackPort?: number,
|
||||||
): Promise<OIDCAuthorize> {
|
): Promise<OIDCAuthorize> {
|
||||||
try {
|
try {
|
||||||
const response = await authApi.get("/users/oidc/authorize", {
|
const response = await authApi.get("/users/oidc/authorize", {
|
||||||
params: { rememberMe },
|
params: { rememberMe, desktopCallbackPort },
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user