Redirect to URL in a custom_logout

Hello to everyone

I’m trying to create a “custom_logout” method where I would like to be redirected to a specific url after the logout.
Following the hook.py content to trigger my “custom_logout” method:

on_logout = “myapp.utils.custom_logout”

Following my “custom_logout” method code in utils.py:

def custom_logout():
# Check if the custom login session variable is set
if frappe.local.session.data.get(“cutom_logout_flag”):
frappe.local.response[“type”] = “redirect”
frappe.local.response[“location”] = “https://mypage.com
frappe.local.response[“status”] = 303 # Use HTTP 303 for a redirect after POST

Well “custom_logout” method is correctly triggered, my problem is the redirect that doesn’t work. Frappe basically keep going to his default login page.
Anyone can give me a clue on how to sort out this?

Unfortunately the default logout action is performed via an API call, so that hook is only useful for server-side functions, and a redirect not possible without JavaScript overrides.

Check out the JavaScript hooks in this (new/dev) app for a example of how to do what you want: GitHub - Avunu/jwt_auth: JWT-based authentication for the Frappe Framework

Following setup is complex and useful only when using multiple apps communicating with each other with shared identity. It may not be useful for session/cookies based logout/login.

It also does jwt using jwks. Used for id_token.

In case of opaque (non id_token) tokens there’s also backchannel logout endpoint that’ll remove token from cache.

App: GitHub - castlecraft/cfe: Castlecraft Extensions for Frappe Framework based Resource Servers
Docs: OAuth 2 and OIDC - Frappe Manual

Refer Custom Auth from same manual, that does some combination of cookies and token issued by frappe oauth client. You can even login from one system which has established identity into frappe. Example: https://gitlab.com/castlecraft/frappe_utils/-/wikis/Login-with-custom-provider

1 Like

Hi Guys
Thanks for your suggestions, but I’m looking for something easy and smart to implement. Nothing of complicated, no repositories or other things that need maintenance!
At the moment I’m dealing with a partial solution that works on server-side and client-side.

Server-Side something like that:

> def custom_logout():
>     #Check if the custom logout session variable is set
>     if frappe.local.session.data.get("cutom_logout_flag"):
>         frappe.local.cookie_manager.set_cookie("custom_logout", "true")

Client-Side something like that:

> document.addEventListener("DOMContentLoaded", function () {
>     if (document.cookie.includes("custom_logout=true")) {
>         document. Cookie = "custom_logout=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
>         window.location.href = "https://mypage.com";
>     }
> });

This solution gave me some results but at moment I’m still looking in something that works better and possibly is cleaner.
Any suggestion is really welcome.

Hey @maverjk, I wasn’t trying to insinuate that you needed something very complicated, and certainly not a full-blown app, just that you could harvest a few design patterns from my code. I could have been much more specific.

The bottom line is that existing Frappe function don’t make the custom redirect on logout very easy, and the mechanisms are distinct between the frontend (portal pages) and the backend (app pages). For the app pages, you need to override the frappe.app.logout JavaScript function. The original function is as follows:

logout() {
	var me = this;
	me.logged_out = true;
	return frappe.call({
		method: "logout",
		callback: function (r) {
			if (r.exc) {
				return;
			}
			me.redirect_to_login();
		},
	});
}

The main issue here is that the logout call performed in the background, and the subsequent redirect_to_login is hard-coded, and can’t be intercepted. The following works as an override, though if anyone has a more direct way to override this function, that would be great:

document.addEventListener('DOMContentLoaded', () => {
	// Wait for frappe to be initialized
	const onFrappeApplication = () => {
		if (window.frappe?.app) {
			// Override the logout function
			frappe.app.logout() = () => {
				var me = this;
				me.logged_out = true;
				return frappe.call({
					method: "logout",
					callback: function (r) {
						if (r.exc) {
							return;
						}
						// Redirect to the custom URL
						window.location.href = "/custom-url";
					},
				});
			};
		} else {
			// Check again in a moment if frappe isn't ready
			setTimeout(onFrappeApplication, 100);
		}
	};

	onFrappeApplication();
});

As I mentioned, the portal/web pages work completely differently. I managed to override the frontend menu with the following hook:

website_context = {
    "post_login": [
        {"label": "My Account", "url": "/me"},
        {"label": "Log out", "url": "/?cmd=jwt_auth.auth.web_logout"}  # Custom logout URL
    ]
}

This is pretty different from the above, as it relies on a custom api endpoint:

@frappe.whitelist()
def web_logout():
	auth = SessionJWTAuth()
	frappe.local.login_manager.logout()
	if auth.settings.enabled:
		location = auth.get_logout_url()
	else:
		location = "/login"
	frappe.local.response["type"] = "redirect"
	frappe.local.response["location"] = location

Again, if anyone has any improvements to suggest on the above, that would be most welcome.

2 Likes

Hi @batonac
Thanks for your code :slight_smile: I had occasion to test it… there is a litte error in the function assignment “frappe.app.logout” that I sorted out as well as the possibility to keep the default “redirect to login” behavior. Sorted these little things your code works great :ok_hand:, it’s really smooth and well structured, the most complete that I found so far to be honest.

By the way, following your code with the mentioned minus adjustments:

Client-Side code in “custom_logout_redirect.js”:

document.addEventListener('DOMContentLoaded', () => {
	// Wait for frappe to be initialized
	const onFrappeApplication = () => {
		if (window.frappe?.app) {
			// Override the logout function
			frappe.app.logout = () => {
				var me = this;
				me.logged_out = true;
				return frappe.call({
					method: "logout",
					callback: function (r) {
						if (r.exc) {
							return;
						}
                        if (document.cookie.includes("custom_logout=true")) {
                            // Delete custom_logout cookie
                            document.cookie = "custom_logout=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
                            // Redirect to the custom URL
                            window.location.href = "https://mypage.com";
                        } else {
                            window.location.href = `/login?redirect-to=${encodeURIComponent(
                                window.location.pathname + window.location.search
                            )}`;
                        }
					},
				});
			};
		} else {
			// Check again in a moment if frappe isn't ready
			setTimeout(onFrappeApplication, 100);
		}
	};

	onFrappeApplication();
});

Server-Side code in “utils.py”:

def custom_logout():
    #Check if the custom logout session variable is set
    if frappe.local.session.data.get("cutom_logout_flag"):
        frappe.local.cookie_manager.set_cookie("custom_logout", "true")

Server-Side code in “hook.py”:

app_include_js = "/assets/myapp/js/custom_logout_redirect.js"
on_logout = "myapp.utils.custom_logout"

Of course, if someone come out with something cleaner and smarter is really welcome :slight_smile:

That looks pretty good. Since you want to introduce variability of responses, you could use my original code, adapted to your example:

Frontend:

document.addEventListener('DOMContentLoaded', () => {
	// Wait for frappe to be initialized
	const onFrappeApplication = () => {
		if (window.frappe?.app) {
			// Override the logout function
			frappe.app.logout = function () {
				var me = this;
				me.logged_out = true;

				return frappe.call({
					method: "myapp.utils.custom_logout",
					callback: function (r) {
						if (r.exc) {
							return;
						}
						window.location.href = r.message.redirect_url;
					}
				});
			};
		} else {
			// Check again in a moment if frappe isn't ready
			setTimeout(onFrappeApplication, 100);
		}
	};

	onFrappeApplication();
});

Backend:

@frappe.whitelist()
def custom_logout():
	frappe.local.login_manager.logout()
	if frappe.local.session.data.get("custom_logout_flag"):
		return {"redirect_url": "https://mypage.com"}
	else:
		return {"redirect_url": "/login"}

The trick here is that I am returning a redirect_url param from the backend, where all the variability is controlled. That feels a bit more direct and (presumably) reliable versus setting a cookie at the last minute, but I don’t know your exact use-case either…

1 Like