Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to logout with Form based AUTH #27389

Closed
tmulle opened this issue Aug 19, 2022 · 22 comments
Closed

How to logout with Form based AUTH #27389

tmulle opened this issue Aug 19, 2022 · 22 comments

Comments

@tmulle
Copy link
Contributor

tmulle commented Aug 19, 2022

We are using Quarkus with FORM based authentication and JDBC backend with the JSF extension so we can use MyFaces/Primefaces.

This is a great link to get Primefaces working on Quarkus!
https://github.com/melloware/quarkus-faces

Everything works fine and I can log in and see the cookie Quarkus-credential be created and all is well with the SecurityIdentity, etc.

Question is, how do I let the user logout? We need to have the ability to manually log out users from our website.

I tried the following code which I found on the web, but I'm suspecting this isn't working because there is no HTTPSession?

According to: https://quarkus.io/guides/security-built-in-authentication#form-auth only a cookie is created?

So, how do I let the user manually log out via a link on the webpage? Am I going to have to write some client-side javascript to manually delete the cookies? I don't want to solely rely on the cookie expirations, etc.

public void logout() {
        FacesContext context = FacesContext.getCurrentInstance();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        try {
            request.logout();
            context.addMessage(null, new FacesMessage("Logged out."));
        } catch (ServletException e) {
            e.printStackTrace();
            context.addMessage(null, new FacesMessage("Logout failed."));
        }
    }
@quarkus-bot
Copy link

quarkus-bot bot commented Aug 19, 2022

/cc @pedroigor, @sberyozkin

@tmulle
Copy link
Contributor Author

tmulle commented Aug 20, 2022

Another related question for LOGIN, right now I'm using the standard form auth post using the HTML below.
That works and properly redirects me to whatever resource I was trying to access if I wan't logged in.

<form action="j_security_check" method="post">
  <div class="container">
    <label for="j_username"><b>Username</b></label>
    <input type="text" placeholder="Enter Username" name="j_username" required>

    <label for="j_password"><b>Password</b></label>
    <input type="password" placeholder="Enter Password" name="j_password" required>

    <button type="submit">Login</button>
  </div>
</form>

If I wanted, How would I accomplish a programmatic login so I can do bean validations on my Login backing bean?

@sberyozkin
Copy link
Member

@tmulle One option could be to add a logoutPath property to FormAuthConfig and update FormAuthenticationMechanism to delete the cookie and redirect to the initial page if the request path matches it. Please consider opening a PR.

@tmulle
Copy link
Contributor Author

tmulle commented Aug 22, 2022

@sberyozkin

I'm looking at the FormAuthenticationMechanism class now and wondering how would the logout path even be called?
Isn't this class automatically called for a login when Quarkus determines that the user hasn't been authenticated yet?

How would the code you suggested be called from a link on a web page, i.e. <a href="/logout.html">Logout</>?

Prior to your response, one thing I tried a few days ago was to delete the cookies on the server side, but the cookies were never deleted. The cookies are being found ok in the server, but setting the maxAge doesn't appear to do anything. I tried initially finding the quarkus-credential cookie and then when that. didn't work, I tried to just delete them all. But they are never deleted.

This code was found in the standard JSF tutorials on how to handle form auth login/logouts.

On my users.xhtml file:

<h:form>
                    <p:commandButton action="#{login.logout}" value="Logout" />
                </h:form>
public String logout() {
        String result = "/public/index?faces-redirect=true";
        
        HttpServletRequest request = (HttpServletRequest) facesContext.getExternalContext().getRequest();
        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
        
        try {
            Cookie[] cookies = request.getCookies();
            for (Cookie c : cookies) {
                System.out.println(c.getName());
                c.setMaxAge(0);
                response.addCookie(c);
//                if (c.getName().equals("quarkus-credential")) {
//                    System.out.println("Deleting cookie");
//                    c.setMaxAge(0);
//                    response.addCookie(c);
//                    break;
//                }
            }
            request.logout();
            facesContext.getExternalContext().invalidateSession();
        } catch (ServletException e) {
            e.printStackTrace();
            result = "/login?faces-redirect=true";
        }
        ```

@sberyozkin
Copy link
Member

sberyozkin commented Aug 22, 2022

@tmulle

How would the code you suggested be called from a link on a web page, i.e. <a href="/logout.html">Logout</>?

FormAuthenticationMechanism will be called for every request - so if the user has already been authenticated then I believe the code goes into this branch:
https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java#L179
So at this point you can check if the request path matches a logout path and if yes then call loginManager.clear() which will clear the cookies and redirect to the starting page

@mkouba
Copy link
Contributor

mkouba commented Aug 23, 2022

Question is, how do I let the user logout? We need to have the ability to manually log out users from our website.

You can also try to "remove" the cookie on the client side and reload the page. I.e. something like document.cookie = "form_cookie_name=; Max-Age=0" and window.location.href = "/".

@sberyozkin This would probably deserve a clarification in the docs. WDYT?

I've also found: https://stackoverflow.com/questions/68640209/quarkus-http-authentication-logout

@sberyozkin
Copy link
Member

sberyozkin commented Aug 23, 2022

@mkouba Sure. We have not really prioritized on the Form authentication (which was introduced in Quarkus to help users already familiar it to get started fast with Quarkus Security) and instead recommend OIDC, but the users are keeping using it, so makes sense to add this hint. I'm not sure though why the code at https://stackoverflow.com/questions/68640209/quarkus-http-authentication-logout does not work... I see, Stuart comments that the path / has to be set first, similarly to your window.location.href = "/"... OIDC also sets the cookie path when removing it...

@tmulle
Copy link
Contributor Author

tmulle commented Sep 4, 2022

@mkouba @sberyozkin

I was able to get the logout working via code if I set the "Path" of the cookie as well as setting the max age. I thought that because I simply read the existing cookies and ONLY overrode the max-age that the original path value would suffice. But it appears I have to also manually set that as well.

After doing so, the "Quarkus-credential" cookie was deleted properly.

public String logout() {
        String result = "/public/home?faces-redirect=true";
        
        HttpServletRequest request = (HttpServletRequest) facesContext.getExternalContext().getRequest();
        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
        
        try {
            Cookie[] cookies = request.getCookies();
            for (Cookie c : cookies) {
                System.out.println(c.getName());
                if (c.getName().equals("quarkus-credential")) {
                    System.out.println("Deleting cookie");
                    c.setPath("/");
                    c.setMaxAge(0);
                    response.addCookie(c);
                    break;
                }
            }
            request.logout();
            facesContext.getExternalContext().invalidateSession();
        } catch (ServletException e) {
            e.printStackTrace();
            result = "/login?faces-redirect=true";
        }
        
        return result;
    }

Now, with LOGIN via code, there seems to be an issue where the "quarkus-credential" cookie is NOT being set in the HttpResponse after successful login when I use the following code, which I found from the official JSF documents.

The weird thing is that my user is successfully logged in but the cookie is not set, so when I try to access a protected resource I'm redirected back to the login page even though the user is logged in already.

Using the standard "/j_security_check" form POST works fine, but we want to login programmatically so we can validate the username/password fields and check to make sure they are entered and display an error on the page itself without being redirected to an error page.

This is the code I am using to attempt to login via code from my JSF page. Like I said, it works, if I enter bad values I get an AuthenticationException on the server so I know it is attempting to validate the credentials. It just doesn't seem to be adding the cookie into the cookie stream to get stored in the browser.

https://docs.oracle.com/javaee/6/tutorial/doc/glxce.html

public String login() {
        try {
            HttpServletRequest request = (HttpServletRequest) facesContext.getExternalContext().getRequest();
            request.login(this.username, this.password);
            Cookie[] cookies = request.getCookies();
            for (Cookie c : cookies) {
                System.out.println(c.getName());
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Invalid Credentials", null));
            PrimeFaces.current().ajax().update("form:messages");
            return null;
        }
        
        return "/public/home?faces-redirect=true";
    }

@tmulle
Copy link
Contributor Author

tmulle commented Sep 13, 2022

@mkouba @sberyozkin

Was just wondering if either of you had a suggestion on how to handle server side logins via code using the standard http mechanism as I mention above?

We'd like to use JFF/Primefaces customized Login forms so we can support validation of fields (missing password, username, etc) BEFORE the form is posted to the server. And the only way I know of is to use a backing bean which calls the code I posted above from the JSF tutorials.

Thanks!

@sberyozkin
Copy link
Member

@michalvavrik Hey Michal, did you happen to fix this issue with your recent Form auth update ? thanks

@michalvavrik
Copy link
Member

michalvavrik commented Feb 16, 2023

@sberyozkin in a sense, yes; there is still only way to logout - get rid of cookies. @tmulle mentions he does that and needs a way to avoid redirect to error page - that is possible now. IMO it's enough as removing cookies on the front end is pretty simple.

@mkouba
Copy link
Contributor

mkouba commented Feb 17, 2023

FTR this code (POST and io.vertx.core.http.HttpServerResponse#removeCookie()) seems to work just fine.

@michalvavrik
Copy link
Member

FTR this code (POST and io.vertx.core.http.HttpServerResponse#removeCookie()) seems to work just fine.

I'd expect it too, thanks for sharing. My personal opinion is that it is so straightforward, that there is no point of mentioning it in docs or providing build in logout endpoint. I don't know how others see it though.

@michalvavrik
Copy link
Member

@sberyozkin ^^^

@jingglang
Copy link

Hi,

How to implement log out on all devices?

@michalvavrik
Copy link
Member

Hi,

How to implement log out on all devices?

You won't as form auth mechanism doesn't keep state, that is saved in a cookie and authenticity of the cookie is verified by the secret key. That's legit question though.

@jingglang
Copy link

For a reference, here is my attempt to implement logout:

@Alternative
@Priority(1)
@ApplicationScoped
public class CustomFormAuthenticationMechanism implements HttpAuthenticationMechanism {
	
	@Inject 
	FormAuthenticationMechanism formAuthenticationMechanism;

	@Override
	public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
		if (context.normalizedPath().endsWith("/logout")) {
			Cookie cookie = context.request().getCookie("quarkus-credential");
			if (cookie != null) {
				cookie.setPath("/");
			}
			context.response().removeCookie("quarkus-credential");
			return Uni.createFrom().optional(Optional.empty());
		}
		return formAuthenticationMechanism.authenticate(context, identityProviderManager);
	}

	@Override
	public Uni<ChallengeData> getChallenge(RoutingContext context) {
		return formAuthenticationMechanism.getChallenge(context);
	}

	@Override
	public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
		return formAuthenticationMechanism.getCredentialTypes();
	}
	
}

@ivangreene
Copy link
Contributor

Here's an example of clearing the cookie with the org.jboss.resteasy.reactive.RestResponse:

    @Path("/logout")
    @POST
    public RestResponse<?> logout(@Context SecurityContext securityContext) {
        if (securityContext.getUserPrincipal() != null) {
            return RestResponse.ResponseBuilder.noContent()
                    .cookie(new NewCookie.Builder("quarkus-credential")
                            .maxAge(0)
                            .expiry(Date.from(Instant.EPOCH))
                            .path("/")
                            .build())
                    .build();
        }
        return RestResponse.ResponseBuilder.create(RestResponse.Status.BAD_REQUEST, "Not authenticated")
                .build();
    }

@melloware
Copy link
Contributor

Here is a RestEasy Classic version..

@Path("/api/logout")
public class LogoutResource {

    @ConfigProperty(name = "quarkus.http.auth.form.cookie-name")
    String cookieName;

    @Inject
    CurrentIdentityAssociation identity;


    @POST
    public Response logout() {
        if (identity.getIdentity().isAnonymous()) {
            throw new UnauthorizedException("Not authenticated");
        }
        final NewCookie removeCookie = new NewCookie.Builder(cookieName)
                .maxAge(0)
                .expiry(Date.from(Instant.EPOCH))
                .path("/")
                .build();
        return Response.noContent().cookie(removeCookie).build();
    }
}

@michalvavrik
Copy link
Member

I propose to close this as logout is now documented #36818 by @melloware and IMO it is not necessary to provide anything OOTB.

@michalvavrik michalvavrik added the triage/consider-closing Bugs that are considered to be closed because too old. Using the label to do a mark and sweep proces label Nov 4, 2023
@melloware
Copy link
Contributor

Yep let's close @tmulle

@tmulle
Copy link
Contributor Author

tmulle commented Nov 6, 2023

ok yeah we can close this now that we have examples.. thanks everyone.

@tmulle tmulle closed this as completed Nov 6, 2023
@michalvavrik michalvavrik removed the triage/consider-closing Bugs that are considered to be closed because too old. Using the label to do a mark and sweep proces label Nov 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants