springspring-bootspring-resttemplate

Endpoint does not respond when I use conversion to an object


I created an endpoint to return an access token to my application. When I convert the json from the token to an object called user, postman keeps processing it and reports a timeout. No errors are displayed in the spring boot log. Strangely, if I convert it to a string, the token is returned normally.

My endpoint:

@RequestMapping("/public/token")
@RestController
public class TokenController {

    @Value("${KEYCLOAK_URL}")
    private String KEYCLOAK_URL;

    private static final String CLIENT_ID = "my-app";
    private static final String GRANTYPE = "password";

    @PostMapping
    public ResponseEntity<User> token(@RequestBody Login login) {

        HttpHeaders headers = new HttpHeaders();
        RestTemplate rt = new RestTemplate();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("client_id", CLIENT_ID);
        formData.add("username", login.username);
        formData.add("password", login.password);
        formData.add("grant_type", GRANTYPE);

        HttpEntity<MultiValueMap<String, String>> entity
                = new HttpEntity<MultiValueMap<String,String>>(formData, headers);

        var user = rt.postForEntity( KEYCLOAK_URL + "/protocol/openid-connect/token", entity, User.class);

        return user;
    }

    public record Login(String username, String password) {}

}

User class

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class User {

    @JsonProperty("access_token")
    private String acessToken;

    @JsonProperty("expires_in")
    private Long expiresIn;

    @JsonProperty("refresh_expires_in")
    private Long refreshExpiresIn;

    @JsonProperty("refresh_token")
    private String refreshToken;

    @JsonProperty("token_type")
    private String tokenType;
}

Postman Postman

The json conversion from token to user is taking place. I couldn't identify the reason for infinite processing when I return this object.


Solution

  • 2 years ago i wrote a Keycloak project for my usage that i think it could be helpful for you

    https://github.com/bizhan-laripour/keycloak-sso

    package com.sso.keycloak.service;
    
    import com.sso.keycloak.dto.*;
    import com.sso.keycloak.exception.CustomException;
    import com.sso.keycloak.mapper.UserMapper;
    import org.apache.http.HttpResponse;
    import org.apache.http.HttpStatus;
    import org.apache.http.NameValuePair;
    import org.apache.http.client.HttpClient;
    import org.apache.http.client.entity.UrlEncodedFormEntity;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.impl.client.HttpClientBuilder;
    import org.apache.http.message.BasicNameValuePair;
    import org.keycloak.admin.client.Keycloak;
    import org.keycloak.admin.client.resource.RealmResource;
    import org.keycloak.admin.client.resource.UserResource;
    import org.keycloak.admin.client.resource.UsersResource;
    import org.keycloak.representations.idm.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    
    import javax.ws.rs.core.Response;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.util.*;
    import java.util.stream.Collectors;
    
    @Service
    public class KeycloakService {
    
    
        @Value("${keycloak.credentials.secret}")
        private String SECRETKEY;
    
        @Value("${keycloak.resource}")
        private String CLIENTID;
    
        @Value("${keycloak.auth-server-url}")
        private String AUTHURL;
    
        @Value("${keycloak.realm}")
        private String REALM;
    
        private final Keycloak keycloak;
    
        private UserMapper userMapper;
    
    
        @Autowired
        public KeycloakService(Keycloak keycloak, UserMapper userMapper) {
            this.keycloak = keycloak;
            this.userMapper = userMapper;
    
        }
    
    
        
    
        public String getToken(UserCredentials userCredentials) {
            String responseToken = null;
            try {
                String username = userCredentials.getUsername();
                List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
                urlParameters.add(new BasicNameValuePair("grant_type", "password"));
                urlParameters.add(new BasicNameValuePair("client_id", CLIENTID));
                urlParameters.add(new BasicNameValuePair("username", username));
                urlParameters.add(new BasicNameValuePair("password", userCredentials.getPassword()));
                urlParameters.add(new BasicNameValuePair("client_secret", SECRETKEY));
                responseToken = sendPost(urlParameters);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return responseToken;
    
        }
    
        private String sendPost(List<NameValuePair> urlParameters) throws Exception {
            HttpClient client = HttpClientBuilder.create().build();
            HttpPost post = new HttpPost(AUTHURL + "/realms/" + REALM + "/protocol/openid-connect/token");
            post.setEntity(new UrlEncodedFormEntity(urlParameters));
            HttpResponse response = client.execute(post);
            BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            StringBuffer result = new StringBuffer();
            String line = "";
            while ((line = rd.readLine()) != null) {
                result.append(line);
            }
            return result.toString();
        }
    
    
        
    

    This class contains methods for getting token

    package com.sso.keycloak.dto;
    
    import java.util.List;
    
    public class SsoResponse {
    
        private String status;
    
        private String statusMessage;
    
        private String accessToken;
    
        private List<Module> modules;
    
        public String getStatus() {
            return status;
        }
    
        public void setStatus(String status) {
            this.status = status;
        }
    
        public String getStatusMessage() {
            return statusMessage;
        }
    
        public void setStatusMessage(String statusMessage) {
            this.statusMessage = statusMessage;
        }
    
        public String getAccessToken() {
            return accessToken;
        }
    
        public void setAccessToken(String accessToken) {
            this.accessToken = accessToken;
        }
    
        public List<Module> getModules() {
            return modules;
        }
    
        public void setModules(List<Module> modules) {
            this.modules = modules;
        }
    }
    
    

    This class is a dto for populating response

    package com.sso.keycloak.dto;
    
    public class UserCredentials {
    
        private String password;
    
        private String username;
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
    }
    

    This class is a dto that contains username and password for authorization

    package com.sso.keycloak.service;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.sso.keycloak.dto.SsoResponse;
    import com.sso.keycloak.dto.UserCredentials;
    import com.sso.keycloak.exception.CustomException;
    import org.apache.http.HttpStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.Map;
    
    @Service
    public class LoginService {
    
        private KeycloakService keycloakService;
    
    
        @Autowired
        public LoginService(KeycloakService keycloakService){
            this.keycloakService = keycloakService;
        }
    
    
        public SsoResponse loginResponse(UserCredentials userCredentials){
            SsoResponse ssoResponse = new SsoResponse();
            String token = keycloakService.getToken(userCredentials);
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                Map<String, String> map = objectMapper.readValue(token, Map.class);
                ssoResponse.setAccessToken(map.get("access_token"));
                ssoResponse.setModules(keycloakService.getRolesOfAUser(userCredentials));
                ssoResponse.setStatus("SUCCESS");
                ssoResponse.setStatusMessage("Request has been processed successfully");
                return ssoResponse;
            }catch (Exception exception){
                throw new CustomException(HttpStatus.SC_BAD_REQUEST , "login failed");
            }
    
        }
    }
    

    This class is A service for getting Token

    package com.sso.keycloak.controller;
    
    import com.sso.keycloak.dto.UserCredentials;
    import com.sso.keycloak.service.KeycloakService;
    import com.sso.keycloak.service.LoginService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping(value = "/login")
    public class LoginController {
    
        private final LoginService loginService;
    
        @Autowired
        public LoginController(LoginService loginService){
            this.loginService = loginService;
        }
    
        @RequestMapping(value = "login" , method = RequestMethod.POST)
        public ResponseEntity<?> login(@RequestBody UserCredentials userCredentials){
    
            return new ResponseEntity<>(loginService.loginResponse(userCredentials), HttpStatus.OK);
        }
    }
    

    And this is a controller for getting ssoResponse in a ResponseEntity