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

FormAuth: Programatically login using Form Authentication #409

Open
Ryaryu opened this issue Mar 7, 2024 · 10 comments
Open

FormAuth: Programatically login using Form Authentication #409

Ryaryu opened this issue Mar 7, 2024 · 10 comments
Labels
question Further information is requested

Comments

@Ryaryu
Copy link

Ryaryu commented Mar 7, 2024

Hello, I'm working on an app that has to use Form Authentication.

Normally we would just create a form posting to /j_security_check and that would work with Quarkus.
But I need to do some work before calling the endpoint, therefore I'm calling a method from a @Named bean.
The problem is that after doing this before-hand work, when I do call ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).login(username, password);, the method works perfectly but Quarkus doesn't generate a Cookie.

Have you found a solution for this?
Currently I'm picking the external host/url and manually calling /j_security_check with an absolute URL, but that is very ugly and error prone.

@melloware
Copy link
Owner

Because Quarkus obsviously works differently than a normal EE container i am not surprised by this.

@tmulle asked this very question on this ticket how to programmtic "login" and I don't think it ever got answered as that topic morphed more into progammatic logout and the login question i am not sure was ever answered was it @tmulle?

Read this thread: quarkusio/quarkus#27389

@melloware melloware added the question Further information is requested label Mar 7, 2024
@melloware
Copy link
Owner

melloware commented Mar 8, 2024

@Ryaryu it might be worth opening another Quarkus ticket and reference that original ticket that was never answered about programmatic login?

I also asked the Devs on Zulip Chat as well: https://quarkusio.zulipchat.com/#narrow/stream/187038-dev/topic/j_security_check.20Programmatic.20Login.3F

@Ryaryu

This comment has been minimized.

@melloware

This comment has been minimized.

@melloware
Copy link
Owner

@Ryaryu can you post a small sample code of how you authenticate and logout with quarkus Faces? There are other users asking how to create a login form etc. we have an OIDC example from @tmulle but not a basic login form authentication example. Even snippets here will be fine?

@Ryaryu
Copy link
Author

Ryaryu commented Mar 28, 2024

Oh... sure.
I'm using a single Bean to handle both.

@Named
@RequestScoped
public class LoginController {

  @Getter
  @Setter
  String username;

  @Getter
  @Setter
  String password;

  @Inject
  FacesContext facesContext;

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

  /**
  * Clear cookieName and redirects to my login page (/login.xhtml)
  */
  public String logout() {
    var fcResponse = (HttpServletResponse) facesContext.getExternalContext().getResponse();
    var cookie = new Cookie(cookieName, "");
    cookie.setMaxAge(0);
    fcResponse.addCookie(cookie);
    return "/login.xhtml?faces-redirect=true";
  }

  public void login() {
    try {
      var request = (HttpServletRequest) facesContext.getExternalContext().getRequest();

      generateCookie(request);

      // redirect to your main page.
      facesContext.getExternalContext().redirect("/principal.xhtml");
    } catch (Exception ex) {
      // do something?
    }
  }

  /**
   * Here we just replace the login form partial URL (in my case /login.xhtml) with /j_security_check
   * and make a request there so Quarkus can create the session cookie
   */
  private void generateCookie(HttpServletRequest request) throws IOException, InterruptedException {
    var securityCheckUrl = request.getRequestURL().toString()
        .replace("/login.xhtml", "/j_security_check");
    var response = jSecurityCheckRequest(securityCheckUrl);

    var fcResponse = (HttpServletResponse) facesContext.getExternalContext().getResponse();
    setCookie(response, fcResponse);
  }

  /**
  * Magic lies here.
  * We set the cookie generated by the /j_security_check request into the FacesContext response.
  */
  private void setCookie(HttpResponse<String> response, HttpServletResponse fcResponse) {
    var responseMap = response.headers().map();
    if (responseMap.containsKey("set-cookie")) {
      var cookieString = responseMap.get("set-cookie").get(0);
      io.undertow.server.handlers.Cookie cookie = io.undertow.util.Cookies.parseSetCookieHeader(cookieString);
      var quarkusCookie = new Cookie(cookieName, cookie.getValue());
      quarkusCookie.setMaxAge(8 * 60 * 60);
      quarkusCookie.setHttpOnly(true);
      fcResponse.addCookie(quarkusCookie);
    }
  }

  private HttpResponse<String> jSecurityCheckRequest(String securityCheckUrl)
      throws IOException, InterruptedException {
    var response = HttpClient.newHttpClient().send(HttpRequest.newBuilder()
        .uri(URI.create(securityCheckUrl))
        .POST(HttpRequest.BodyPublishers.ofString(
            "j_username=" + username + "&j_password=" + password))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .build(), HttpResponse.BodyHandlers.ofString());
    return response;
  }

}

You can then call this bean from your xhtml freely, #{loginController.login()} or #{loginController.logout().

@melloware melloware changed the title Programatically login using Form Authentication FormAuth: Programatically login using Form Authentication Nov 17, 2024
@melloware
Copy link
Owner

Issue created at Quarkus HTTP: quarkusio/quarkus-http#169

@melloware
Copy link
Owner

melloware commented Nov 19, 2024

@Ryaryu this caused a bug found in the other ticket the proper way to parse the cookie.

var cookieString = responseMap.get("set-cookie").get(0);
io.undertow.server.handlers.Cookie cookie = io.undertow.util.Cookies.parseSetCookieHeader(cookieString);
var quarkusCookie = new Cookie(cookieName, cookie.getValue());

I updated your code above.

@tandraschko
Copy link

tandraschko commented Jan 31, 2025

Here is my solution, AbstractIdendityService is my own service:

quarkus.http.auth.basic=true
quarkus.http.auth.form.enabled=true
quarkus.http.auth.form.login-page=/login
quarkus.http.auth.form.landing-page=/dashboard
quarkus.http.auth.form.error-page=/login?failed

quarkus.http.auth.permission.public.paths=/jakarta.faces.resource/*,login,error,notfound,denied,expired,help,favicon.ico
quarkus.http.auth.permission.public.policy=permit
quarkus.http.auth.permission.public.methods=GET

quarkus.http.auth.permission.private.paths=/*
quarkus.http.auth.permission.private.policy=authenticated
                <form id="j_security_check"  action="/j_security_check" method="post">
                    <div class="login-form">
                        <h2>Login</h2>

                        <p:staticMessage severity="error"
                                         summary="#{msgs['login.error.title']}"
                                         detail="#{msgs['login.error.description']}"
                                         rendered="#{request.parameterMap.containsKey('failed')}" />

                        <p:inputText id="j_username" name="j_username" placeholder="#{msgs['login.username']}" required="true" />
                        <p:password id="j_password" name="j_password" placeholder="#{msgs['login.password']}" />
                        <p:commandButton value="#{msgs['login.submit']}" type="submit" form="@(#j_security_check)" ajax="false" />
                    </div>
                </form>
@ApplicationScoped
public class InitialIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest> {

    @Inject
    AbstractIdendityService<?> idendityService;

    @Override
    public Class<UsernamePasswordAuthenticationRequest> getRequestType() {
        return UsernamePasswordAuthenticationRequest.class;
    }

    @Override
    public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest request,
                                              AuthenticationRequestContext authenticationRequestContext) {
        String username = request.getUsername();
        String password = new String(request.getPassword().getPassword());

        AbstractIdendityService.Idendity idendity = idendityService.authenticate(username, password);
        if (idendity == null) {
            throw new AuthenticationFailedException();
        }

        return Uni.createFrom().item(
                QuarkusSecurityIdentity.builder()
                        .setPrincipal(new QuarkusPrincipal(idendity.getId()))
                        .addCredential(request.getPassword())
                        .setAnonymous(false)
                        .addAttributes(idendity.getAttributes())
                        .addRoles(idendity.getRoles())
                        .build());
    }
}
@ApplicationScoped
public class IntrospectionIdentityProvider implements IdentityProvider<TrustedAuthenticationRequest> {

    @Inject
    AbstractIdendityService<?> idendityService;

    @Inject
    HttpConfiguration httpConfiguration;

    @Override
    public Class<TrustedAuthenticationRequest> getRequestType() {
        return TrustedAuthenticationRequest.class;
    }

    @Override
    public Uni<SecurityIdentity> authenticate(TrustedAuthenticationRequest request,
                                              AuthenticationRequestContext authenticationRequestContext) {

        AbstractIdendityService.Idendity idendity = idendityService.isAuthenticated(request.getPrincipal());
        if (idendity == null) {
            // we received a cookie with old login
            // we need to remove the cookie, otherwise we get a infinite loop
            RoutingContext routingContext = (RoutingContext) request.getAttributes().get(HttpSecurityUtils.ROUTING_CONTEXT_ATTRIBUTE);
            routingContext.response().removeCookie(httpConfiguration.auth.form.cookieName);
            throw new AuthenticationFailedException();
        }

        return Uni.createFrom().item(
                QuarkusSecurityIdentity.builder()
                        .setPrincipal(new QuarkusPrincipal(idendity.getId()))
                        .setAnonymous(false)
                        .addAttributes(idendity.getAttributes())
                        .addRoles(idendity.getRoles())
                        .build());
    }
}

and some logout controller:

@Named
@RequestScoped
public class IdentityFacesController {

    @Inject
    AbstractIdendityService<?> idendityService;

    @Inject
    HttpConfiguration httpConfiguration;

    public void logout() throws IOException {
        AbstractIdendityService.Idendity idendity = idendityService.getCurrentIdentity();

        FacesContext facesContext = FacesContext.getCurrentInstance();
        HttpServletRequest request = (HttpServletRequest) facesContext.getExternalContext().getRequest();
        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
        HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(false);

        try {
            request.logout();
        }
        catch (ServletException e) {
            Log.error("Error while calling logout", e);
        }

        session.invalidate();

        Cookie authCookie = (Cookie) facesContext.getExternalContext().getRequestCookieMap().get(httpConfiguration.auth.form.cookieName);
        if (authCookie != null) {
            authCookie.setPath("/");
            authCookie.setMaxAge(0);
            response.addCookie(authCookie);
        }

        idendityService.remove(idendity);

        facesContext.getExternalContext().redirect(httpConfiguration.auth.form.landingPage.get() + "?faces-redirect=true");
    }
@ApplicationScoped
public abstract class AbstractIdendityService<T extends AbstractIdendityService.Idendity>
        implements IdentityService {

    @Getter
    protected final Map<String, T> identities = new ConcurrentHashMap<>();

    @Inject
    SecurityIdentity securityIdentity;

    public T getCurrentIdentity() {
        String id = securityIdentity.getPrincipal().getName();
        return getIdentities().get(id);
    }

    @Override
    public String getCurrentIdentityId() {
        return securityIdentity.getPrincipal().getName();
    }

    public abstract T authenticate(String username, String password);

    public Idendity isAuthenticated(String id) {
        return identities.get(id);
    }

    public void remove(Idendity idendity) {
        identities.remove(idendity.getId());
        try {
            release((T) idendity);
        }
        catch (Exception e) {
            Log.error("Could not release identity " + idendity.id, e);
        }
    }

    protected abstract void release(T identity);

    public void removeAll() {
        for (T identity : new ArrayList<>(identities.values())) {
            remove(identity);
        }
    }

    public void onShutdown(@Observes ShutdownEvent shutdownEvent) {
        removeAll();
    }

    @Getter
    @Setter
    @EqualsAndHashCode
    @AllArgsConstructor
    public static class Idendity {
        private String id;
        private String username;
        private Set<String> roles;
        private Map<String, Object> attributes;
    }
}

@melloware
Copy link
Owner

Awesome @tandraschko !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants