Home > Back-end >  Spring boot change default authentication system
Spring boot change default authentication system

Time:04-04

Be default spring boot utilizes the HttpServletRequest that accepts value from the client end defaults to "/login" route.

If I I want to create a custom authentication system with options like:

  1. signin endpoint like: "/api/v1/auth/sign" that will accept email and password
  2. rather than creating CustomAuthFilter as shown by many videos in YT create a method in auth controller that handles sending back the jwt tokens.

Now I already know that for changing the default login route I need:

.formLogin().loginProcessingUrl("/api/v1/login")

But what about the next part?

Do I need to create objects like SignInRequest and SignInResponse?

If so will the client applications need to map data in accordance to the SignInRequest and SignInResponse?

This is my Signup service:

@Override
public User signup(User user) {
    String encodedPassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodedPassword);
    return authRepository.save(user);
}

I want to create a similar service for signin like:

@Override
public User signin(String email, String password) {
   // somehow do login and return the user with access and refresh tokens?
}

Even if I create a SigninRequest object the client application will always send the email and password right?

Since I haven't worked with complex backends I have very limited idea on how to solve this issues.

Any insight or resource would be helpful thanks.

My current attemptAuthentication method:

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        StringBuffer sb = new StringBuffer();
        BufferedReader reader = null;
        String content = "";
        String email = "";
        String password = "";
        try {
            reader = request.getReader();
            char[] buffer = new char[1024];
            int read;
            while ((read = reader.read(buffer, 0, buffer.length)) != -1) {
                sb.append(buffer, 0, read);
            }
            content = sb.toString();
            Map<String, String> map = new ObjectMapper().readValue(content, Map.class);
            email = map.get("email");
            password = map.get("password");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException ex) {
                    try {
                        throw ex;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
        return authenticationManager.authenticate(authenticationToken);
    }

CodePudding user response:

Spring supports many sophisticated custom authentication methods. A full overview is available in the spring security documentation:

Spring Security Docs - Authentication

From your question I conclude that you want to stick to some sort of user/password authentication. This is described in the docs here:

Spring Security Docs - User/Password Authentication

You basically have three options:

  1. Form Login: Here, a dynamically generated HTML page is presented to the user, where he can enter user (in your case the email address) and password.
  2. Basic Authentication: Here is no need for a separate HTML page. Instead the browser presents a popup dialog directly, where the user enters his credentials.
  3. Digest Authentication: Rather uncommon and not recommended by spring.org.

Again, from your question I conclude that you want to stick to form login. Spring offers many options for this alternative, which are extensively covered in the above docs.

Another good starting point is the following popular tutorial:

Baeldung Tutorial about Spring Security Form Login

Apart from the mere implementation aspects, here are of course a lot of security considerations involved in the question of authentication methods: User/password authentication is considered a rather insecure form of authentication, as it requires the exchange of a secret between the user and the server. More secure forms are e.g. use one time passwords or certificates (e.g. SSL client certificated which are handled by the Browser). As a starting point for further reading, I can suggest:

Rising Stack - Web Authentication Methods Explained

Port Swigger - Authentication vulnerabilities

For more clarification, I have added here some example code how to do a form login with a react client. This example is based on the reactive WebTestClient in the Spring Framework.

A form login involves the following steps:

  1. A client sends a request (GET or POST) with the wanted URI (could be a static or dynamic HTML page or REST resource).
  2. When the server identifies that the requested URI requires authentication he responds with HTTP redirect status code (302) which points to the login page.
  3. Now the client initiates the login sequence by sending a GET request to the login page. Whereas steps 1 and 2 are optional and may be omitted, this step is crucial, as it initiates a session on the server.
  4. When receiving the request to the login page, the server creates a new HTTP session, a CSRF token (see Spring doc), and it generates the dynamic login page.
  5. The client replies with a POST request which includes user/password and the CSRF token in the body and the session id in the reuest header (e.g. as cookie).
  6. When everything is correct and steps 1 and 2 where not omitted, the server responds with a redirect to the originally requested resource from step 1 (or to the default landing page, if step 1 and 2 where omitted).
  7. Now the client can request the wanted resource again. For all following requests it must include the session id in the request header.

Here now a test client implementation in Java which performs this sequence on the client side:

public class WebTestClientUtil {

    public static class ResponseHolder {
        public String baseUrl;
        public HttpStatus status;
        public String body;
        public String sessionId;
        public String csrfToken;
        public String location;

        public ResponseHolder(boolean sslEnabled, int port) {
            this.baseUrl = getBaseUrl(sslEnabled, port);
        }
    }

    public enum SessionIdResolutionMethod {
        HEADER,
        COOKIE,
        URL
    }

    /**
     * Determine the server base url
     *
     * @param port of the server to connect to
     * @return the server url
     */
    static public String getBaseUrl(boolean sslEnabled, int port) {
        return "http"   (sslEnabled ? "s" : "")   "://localhost:"   port   "/";
    }

    /**
     * Build a client for testing
     *
     * @param ref the response holder to be used for further communication
     * @return the client
     */
    public static WebTestClient buildClient(ResponseHolder ref) {
        return WebTestClient
                .bindToServer()
                .baseUrl(ref.baseUrl)
                .responseTimeout(Duration.ofMinutes(10))  // uncomment for debugging
                .build();
    }

    /**
     * Perform a form login and return a response spec to formulate expectations about the result
     *
     * @param client   the client to be used for connecting to the server
     * @param uri      for the request
     * @param username for authentication
     * @param password for authentication
     * @param ref      container for the server response
     * @return the response spec
     */
    public static WebTestClient.ResponseSpec performLoginSequence(
            WebTestClient client,
            String uri,
            String username,
            String password,
            ResponseHolder ref) {
        return performLoginSequence(client, uri, username, password, ref, SessionIdResolutionMethod.HEADER);
    }

    /**
     * Perform a form login and return a response spec to formulate expectations about the result
     *
     * @param client                    the client to be used for connecting to the server
     * @param uri                       for the request
     * @param username                  for authentication
     * @param password                  for authentication
     * @param ref                       container for the server response
     * @param sessionIdResolutionMethod true if cookies shall be used for session id resolution
     * @return the response spec
     */
    public static WebTestClient.ResponseSpec performLoginSequence(
            WebTestClient client,
            String uri,
            String username,
            String password,
            ResponseHolder ref,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        System.out.println();
        System.out.println("----------------------------------------------------------------------");
        System.out.println("New login sequence initiated for user "   username   " with password "   password   "...");
        System.out.println("----------------------------------------------------------------------");

        // Send api request
        evaluateResponse(client.get().uri(uri).exchange(), ref, sessionIdResolutionMethod)
                .expectStatus().is3xxRedirection()
                .expectHeader().location(ref.baseUrl   "login");

        // Send login request
        evaluateResponse(getRequestSpec(client, "login", ref, sessionIdResolutionMethod)
                .accept(MediaType.TEXT_HTML)
                .acceptCharset(StandardCharsets.UTF_8)
                .exchange(), ref, sessionIdResolutionMethod)
                .expectStatus().isOk();
        assertThat(ref.body).contains("name=\"username\"").contains("name=\"password\"").contains("name=\"_csrf\"");
        assertThat(ref.csrfToken).isNotNull();

        // Send login details
        WebTestClient.ResponseSpec response =
                evaluateResponse(postRequestSpec(client, "login", ref, sessionIdResolutionMethod)
                        .body(BodyInserters
                                .fromFormData("_csrf", ref.csrfToken)
                                .with("username", username)
                                .with("password", password))
                        .exchange(), ref, sessionIdResolutionMethod);

        // In case of errors abort login process
        System.out.println("Login sequence completed...");
        System.out.println("----------------------------------------------------------------------");
        if (ref.status != HttpStatus.FOUND) return response;
        if (!ref.location.equals(ref.baseUrl   uri)) return response;

        // After successful login continue with redirect
        System.out.println("Continuing with original request "   uri   "...");
        return evaluateRedirect(client, ref, sessionIdResolutionMethod);
    }

    public static WebTestClient.ResponseSpec evaluateResponse(WebTestClient.ResponseSpec response, ResponseHolder r) {
        return evaluateResponse(response, r, SessionIdResolutionMethod.HEADER);
    }

    public static WebTestClient.ResponseSpec evaluateResponse(
            WebTestClient.ResponseSpec response,
            ResponseHolder r,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        ExchangeResult result = response
                .expectBody(String.class).consumeWith(v -> r.body = v.getResponseBody())
                .returnResult();
        r.status = result.getStatus();
        System.out.println();
        System.out.println("Response for request "   result.getUrl());
        System.out.println("  HTTP status = "   r.status);

        String sessionId = null;
        switch (sessionIdResolutionMethod) {
            case HEADER, URL -> sessionId = result.getResponseHeaders().getFirst(ResponseHeaderFilter.SESSION_ID_HEADER_NAME);
            case COOKIE -> {
                ResponseCookie sessionIdCookie = result.getResponseCookies().getFirst("SESSION");
                if (sessionIdCookie != null) sessionId = sessionIdCookie.getValue();
            }
        }
        if (sessionId == null) {
            System.out.println("  Session id not set in "   sessionIdResolutionMethod  
                    " - continuing with old ("   r.sessionId   ")");
        } else {
            r.sessionId = sessionId;
            System.out.println("  Session id --> "   r.sessionId);
        }

        if (r.body == null) r.body = "";
        String csrfToken = getCsrfToken(r.body);
        if (csrfToken != null) r.csrfToken = csrfToken;
        csrfToken = result.getResponseHeaders().getFirst(ResponseHeaderFilter.CSRF_HEADER_NAME);
        if (csrfToken != null) {
            csrfToken = result.getResponseHeaders().getFirst(csrfToken);
            if (csrfToken != null) r.csrfToken = csrfToken;
        }
        System.out.println("  CSRF token = "   csrfToken   " --> "   r.csrfToken);

        System.out.println("  Response Headers:");
        for (Map.Entry<String, List<String>> header : result.getResponseHeaders().entrySet()) {
            System.out.println("    "  
                    header.getKey()   ": "  
                    String.join("; ", header.getValue()));
        }
        r.location = result.getResponseHeaders().getFirst("Location");

        System.out.println("  Body:\n"   r.body);
        return response;
    }

    public static WebTestClient.RequestHeadersSpec<?> getRequestSpec(
            WebTestClient client,
            String uri,
            ResponseHolder ref,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        switch (sessionIdResolutionMethod) {
            case HEADER -> {
                WebTestClient.RequestHeadersSpec<?> request = client.get().uri(uri);
                return request.header(ResponseHeaderFilter.SESSION_ID_HEADER_NAME, ref.sessionId);
            }
            case COOKIE -> {
                WebTestClient.RequestHeadersSpec<?> request = client.get().uri(uri);
                return request.cookie("SESSION", ref.sessionId);
            }
            case URL -> {
                return client.get().uri(uri  
                        (uri.contains("?") ? "&" : "?")  
                        "xAuthToken="  
                        new String(Base64.getEncoder().encode(ref.sessionId.getBytes())));
            }
        }
        throw new IllegalArgumentException("Invalid session id resolution method: "   sessionIdResolutionMethod);
    }

    public static WebTestClient.RequestBodySpec postRequestSpec(
            WebTestClient client,
            String uri,
            ResponseHolder ref,
            SessionIdResolutionMethod sessionIdResolutionMethod) {

        switch (sessionIdResolutionMethod) {
            case HEADER -> {
                WebTestClient.RequestBodySpec request = client.post().uri(uri);
                return request.header(ResponseHeaderFilter.SESSION_ID_HEADER_NAME, ref.sessionId);
            }
            case COOKIE -> {
                WebTestClient.RequestBodySpec request = client.post().uri(uri);
                return request.cookie("SESSION", ref.sessionId);
            }
            case URL -> {
                return client.post().uri(uri  
                        (uri.contains("?") ? "&" : "?")  
                        "xAuthToken="  
                        new String(Base64.getEncoder().encode(ref.sessionId.getBytes())));
            }
        }
        throw new IllegalArgumentException("Invalid session id resolution method: "   sessionIdResolutionMethod);
    }

    public static String getCsrfToken(String body) {
        int pos = body.indexOf("name=\"_csrf\"");
        if (pos < 0) return null;
        int start = body.indexOf("value=\"", pos)   7;
        if (start < 0) return null;
        int end = body.indexOf("\"", start);
        if (end < 0) return null;
        return body.substring(start, end);
    }

    @SuppressWarnings("UnusedReturnValue")
    public static WebTestClient.ResponseSpec evaluateRedirect(WebTestClient client, ResponseHolder r) {
        return evaluateRedirect(client, r, SessionIdResolutionMethod.HEADER);
    }

    public static WebTestClient.ResponseSpec evaluateRedirect(WebTestClient client, ResponseHolder r, SessionIdResolutionMethod sessionIdResolutionMethod) {
        return evaluateResponse(getRequestSpec(client, r.location, r, sessionIdResolutionMethod)
                .accept(MediaType.APPLICATION_JSON)
                .acceptCharset(StandardCharsets.UTF_8)
                .exchange(), r, sessionIdResolutionMethod);
    }

    public static void performLogout(WebTestClient client, ResponseHolder r) {
        performLogout(client, r, SessionIdResolutionMethod.HEADER);
    }

    public static void performLogout(WebTestClient client, ResponseHolder r, SessionIdResolutionMethod sessionIdResolutionMethod) {
        evaluateResponse(postRequestSpec(client, r.baseUrl   "logout", r, sessionIdResolutionMethod)
                .acceptCharset(StandardCharsets.UTF_8)
                .exchange(), r, sessionIdResolutionMethod)
                .expectStatus().is3xxRedirection();
        assertThat(r.location).isEqualTo(r.baseUrl   "login?logout");
    }
}
  • Related