How to send received jsessionid via spring 4 resttemplate - java

I'm writing an messenger with JavaFX and Spring4 on client-site and Spring4 on server-site. I secured the server with spring-security 3.2. Now my Problem: I have a loginpage on the client witch sends the login information to spring-security and receive the JSESSIONID cookie. This works fine but when I try to send the JSESSIONID with my request I become an
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class org.messenger.rest.JSONConversationResult] and content type [text/html;charset=UTF-8]
Server Inizializer
public class SpringMvcInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] {ApplicationConfig.class};
}
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] {WebConfig.class};
}
#Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
Server SecurityInizializer
public class SpringSecurityInitializer extends
AbstractSecurityWebApplicationInitializer {
}
Server SecurityConfig
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DriverManagerDataSource dataSource;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String authQuery = "select userid, authority from user where userid = ?";
String userQuery = "select userid, pw, enabled from user where userid = ?";
auth.jdbcAuthentication().dataSource(dataSource)
.passwordEncoder(passwordEncoder())
.usersByUsernameQuery(userQuery)
.authoritiesByUsernameQuery(authQuery);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/getconvs", "/getcontacts").hasRole("USER")
.and()
.formLogin()
.and()
.csrf().disable();
}
#Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new de.daschner.messenger.security.AuthenticationEntryPoint();
}
#Bean
public SuccessHandler successHandler() {
return new SuccessHandler();
}
#Bean
public SimpleUrlAuthenticationFailureHandler failureHandler() {
return new SimpleUrlAuthenticationFailureHandler();
}
#Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(11);
}
}
Server requestmapping for the secured "page"
#RequestMapping(value="/getconvs", method={RequestMethod.GET},
produces={MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody JSONConversationResult getConvsList(HttpServletRequest request, #RequestParam(value="uid") String uid){
JSONConversationResult ret = new JSONConversationResult();
Map<String, Map<Date, String>> convs = convService.getConvsList(uid);
if (convs != null) {
ret.setConversations(convs);
ret.setMessage("OK");
ret.setError(0);
} else {
ret.setError(1);
ret.setMessage("Verbindungsfehler");
}
return ret;
}
Client send Login and get Cookie
Map<String, String> loginform = new HashMap<String, String>();
loginform.put("username", user);
loginform.put("password", pw);
HttpEntity<Map<String, String>> login = new HttpEntity<Map<String, String>>(loginform);
ResponseEntity<HttpServletResponse> response = restTemplate.exchange(
"http://localhost:8080/messenger-webapp/login",
HttpMethod.POST,
login,
HttpServletResponse.class);
HttpHeaders headers = response.getHeaders();
Set<String> keys = headers.keySet();
String cookie = "";
for (String header : keys) {
if (header.equals("Set-Cookie")) {
cookie = headers.get(header).get(0);
}
}
String jsessionid = cookie.split(";")[0];
conf.setJsessionid(jsessionid.split("=", 2)[1]);
return ret;
Client send JSESSIONID with request
ResponseEntity<JSONConversationResult> response = restTemplate.exchange(
"http://localhost:8080/messenger-webapp/getconvs?uid=" + uid,
HttpMethod.GET,
getAuthHeader(),
JSONConversationResult.class);
JSONConversationResult ret = response.getBody();
return ret;
private HttpEntity<String> getAuthHeader() {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", "JSESSIONID=" + config.getJsessionid());
return new HttpEntity<String>(requestHeaders);
}
I hope you can help me.
EDIT:
Ok I figured out that the problem was not that the JSESSIONID wasn't sent correctly. But my login was incorrect and my query to get the user from database.
The correct login-post
ClientHttpResponse response = restTemplate.execute(
"http://localhost:8080/messenger-webapp/login",
HttpMethod.POST,
new RequestCallback() {
#Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getBody().write(("username=" + user + "&password=" + pw).getBytes());
}
},
new ResponseExtractor<ClientHttpResponse>() {
#Override
public ClientHttpResponse extractData(ClientHttpResponse response)
throws IOException {
return response;
}
});
The correct query
String authQuery = "select u.userid, r.role_name from user u, role r, user_role a where u.dbid = a.user_id and r.dbid = a.role_id and u.userid = ?";
I hope this will help other people. If anyone has an alternative please let me know.

Ok I figured out that the problem was not that the JSESSIONID wasn't sent correctly. But my login was incorrect and my query to get the user from database.
The correct login-post
ClientHttpResponse response = restTemplate.execute(
"http://localhost:8080/messenger-webapp/login",
HttpMethod.POST,
new RequestCallback() {
#Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getBody().write(("username=" + user + "&password=" + pw).getBytes());
}
},
new ResponseExtractor<ClientHttpResponse>() {
#Override
public ClientHttpResponse extractData(ClientHttpResponse response)
throws IOException {
return response;
}
});
The correct query
String authQuery = "select u.userid, r.role_name from user u, role r, user_role a where u.dbid = a.user_id and r.dbid = a.role_id and u.userid = ?";
I hope this will help other people. If anyone has an alternative please let me know.

Related

Spring Rest Forbidden Access when hit Exception

I'm using Spring Boot 3.0. The authorization just works as expected but when it hit SomeException like MethodArgumentNotValidException it just only show 403 Forbidden Access with empty body. Before I'm using Spring Boot 3.0 everything just work as I'm expected when hitting Exception like they give me the exception JSON result.
SecurityConfiguration
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
var secret = System.getProperty("app.secret");
var authorizationFilter = new AuthorizationFilter(secret);
var authenticationFilter = new AuthenticationFilter(secret, authenticationManager);
authenticationFilter.setFilterProcessesUrl("/login");
authenticationFilter.setPostOnly(true);
return http
.cors().and()
.csrf((csrf) -> csrf.disable())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login/**", "/trackers/camera/**").permitAll()
.requestMatchers("/sites/**").hasAnyRole(Role.OWNER.name())
.anyRequest().authenticated()
)
.addFilter(authenticationFilter)
.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
var all = Arrays.asList("*");
config.setAllowedOrigins(all);
config.setAllowedHeaders(all);
config.setAllowedMethods(all);
config.setExposedHeaders(all);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
AuthenticationFilter
#RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final String secretToken;
private final AuthenticationManager authenticationManager;
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
try {
var requestMap = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
username = requestMap.getUsername();
password = requestMap.getPassword();
} catch (Exception e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
var token = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
var user = (UserDetails) authResult.getPrincipal();
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var token = JWT.create()
.withSubject(user.getUsername())
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
var jsonMap = new HashMap<String, String>();
jsonMap.put("token", token);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), jsonMap);
response.flushBuffer();
}
}
AuthorizationFilter
#RequiredArgsConstructor
public class AuthorizationFilter extends OncePerRequestFilter {
private final String secretToken;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var authentication = request.getHeader(HttpHeaders.AUTHORIZATION);
if(authentication != null) {
if(authentication.startsWith("Bearer")) {
var token = authentication.substring("Bearer ".length());
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var verifier = JWT.require(algorithm).build();
var message = verifier.verify(token);
var subject = message.getSubject();
var roles = message.getClaim("roles").asArray(String.class);
var authorities = new ArrayList<SimpleGrantedAuthority>();
Arrays.stream(roles).forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
var authenticationToken = new UsernamePasswordAuthenticationToken(subject, token, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else if(authentication.startsWith("Basic")) {
var token = authentication.substring("Basic ".length());
var bundle = new String(Base64.getDecoder().decode(token)).split(":", 2);
if(bundle.length == 2 && bundle[0].equals(System.getProperty("app.camera.username")) && bundle[1].equals(System.getProperty("app.camera.password"))) {
var authenticationToken = new UsernamePasswordAuthenticationToken("camera1", null, Arrays.asList(new SimpleGrantedAuthority(Role.USER.getAuthority())));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
If I understood you correctly, you want the exception-message shown in the return body of the request.
I solved this problem by implementing a (global) exception handler.
(Optional) Create a custom exception, extending some sort of other exception.
public class ApiException extends RuntimeException {
// Not really needed here, as Throwable.java has a message too
// I added it for better readability
#Getter
private String message;
public ApiException() {
super();
}
public ApiException(String message) {
this.message = message;
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
}
(Optional) A Wrapper, with custom information. (This is the object returned in the body).
// I've used a record, as the wrapper simply has to store data
public record ApiError(String message, HttpStatus status, Throwable cause, ZonedDateTime timestamp) {}
The handler
To create the handler, you simply have to create a custom class, which extends the ResponseEntityExceptionHandler.java
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
// The annotation's value can be replaced by any exception.
// Use Throwable.class to handle **all** exceptions.
// For this example I used the previously created exception.
#ExceptionHandler(value = { ApiException.class })
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleApiRequestException(ApiException e) {
// At this point, you can create the exception wrapper to create a
// formatted JSON-response, but you could also just get the info
// required from the exception and return that.
ApiError error = new ApiError(
e.getMessage(),
HttpStatus.BAD_REQUEST,
null,
ZonedDateTime.now(ZoneId.of("Z"))
);
return new ResponseEntity<>(error, error.status());
}
}
Also: To handle different kinds of exceptions differently, like e.g. you want a ApiException to return a 403 and a FooException to return a 404, just create another method inside of the handler and adjust it to your likings.
I hope this helped!
Cheers

Return custom error message to login entry point with eventListeners

In a rest API, i implemented 2 event listners to handle Authentication success and failure. It works fine and I do have a 403 error but i want to return a JSON Message.
For my login I implemented, the following :
#PostMapping("/login")
public ResponseEntity<UserResponse> loadUserByUsername(#RequestBody UserDetailsRequestModel userDetails) {
if(userDetails.getEmail().isEmpty() || userDetails.getPassword().isEmpty()) {
throw new UserServiceException(ErrorMessages.MISSING_REQUIRED_FIELD.getErrorMessage());
}
authenticate(userDetails.getEmail(), userDetails.getPassword());
UserResponse userRestResponseModel = new UserResponse();
ModelMapper modelMapper = new CustomMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STANDARD);
UserDto loggedInUser = userService.getUser(userDetails.getEmail());
userRestResponseModel = modelMapper.map(loggedInUser, UserResponse.class);
// retrieve authorities manually
for(RoleDto roleDto: loggedInUser.getRoles()) {
Collection<AuthorityDto> authorityDtos = authorityService.getRoleAuthorities(roleDto);
roleDto.setAuthorities(authorityDtos);
}
UserPrincipalManager userPrincipal = new UserPrincipalManager(modelMapper.map(loggedInUser, UserEntity.class));
// authorities are not fetched ... so we'll fetch them manually
HttpHeaders jwtHeader = getJwtHeader(userPrincipal);
ResponseEntity<UserResponse> returnValue =
new ResponseEntity<>(userRestResponseModel, jwtHeader, HttpStatus.OK);
return returnValue;
}
private void authenticate(String userName, String password) {
AuthenticationManager authenticationManager =
(AuthenticationManager) SpringApplicationContext.getBean("authenticationManager");
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
}
private HttpHeaders getJwtHeader(UserPrincipalManager userPrincipal) {
HttpHeaders headers = new HttpHeaders();
String token = jwtTokenProvider.generateJwtToken(userPrincipal);
headers.add(SecurityConstants.TOKEN_PREFIX, token);
return headers;
}
#Component
public class AuthenticationFailureListener {
private final LoginAttemptService loginAttemptService;
#Autowired
public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) throws ExecutionException {
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.addUserToLoginAttemptCache(username);
}
}
}
In my loginAttemptService I try to prepare a return to a rest response.
#Override
public void addUserToLoginAttemptCache(String username) {
int attempts = 0;
try {
attempts = SecurityConstants.AUTH_ATTEMPT_INCREMENT + loginAttemptCache.get(username);
loginAttemptCache.put(username, attempts);
String message = "";
if(!errorContext.isHasExceededMaxAttempts()) {
message = "Invalid email or password. You tried : " + attempts + "/" + SecurityConstants.MAX_AUTH_ATTEMPTS;
} else {
message = "You reached " + attempts + " attempts. Account is now locked for " + SecurityConstants.LOCK_DURATION + " min";
}
throw new SecurityServiceException(message);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
My issue is the following: using ControllerAdvice won't work because the error is handled before it could reach it. How can I then return a JSON response to the client ?
I did find a trick for this issue. I created a ManagedBean class
#Data
#Builder
#AllArgsConstructor #NoArgsConstructor
#ManagedBean #ApplicationScope
public class ServletContext {
private HttpServletRequest request;
private HttpServletResponse response;
}
I inject it in my AuthenticationFilter custom class. Here in my attemptAuthentication method I can get access to HttpServletRequest and HttpServletResponse objects. I just have to set my ServletContext object with the request and the response.
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// we may need to pass request and response object if we fail authentication,
servletContext.setRequest(request);
servletContext.setResponse(response);
// spring tries to authenticate user
try {
UserLoginRequestModel creds = new ObjectMapper()
.readValue(request.getInputStream(), UserLoginRequestModel.class);
// we return authentication with email and password
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
new ArrayList<>()
)
);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
Now in my AuthenticationFailureListener, I also inject my ServletContext class and retrieve the values in the method that handle onAuthenticationFailure:
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) throws ExecutionException, IOException {
System.out.println(event);
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.addUserToLoginAttemptCache(username);
int attempts = loginAttemptService.getLoginAttempts(username);
String message;
if(!loginAttemptService.hasExceededMaxAttempts(username)) {
message = "Invalid email or password. You tried : " + attempts + "/" + SecurityConstants.MAX_AUTH_ATTEMPTS;
} else {
message = "You reached " + attempts + " attempts. Account is now locked for " + SecurityConstants.LOCK_DURATION + " min";
}
ErrorMessageResponse errorMessageResponse = new ErrorMessageResponse(new Date(), message);
HttpServletResponse response = servletContext.getResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), errorMessageResponse);
}
}
At this stage, I do have HttpServletResponse object and I can use it to write value. I do believe there may be more elegant ways to handle this, but it works fine.
You can use .accessDeniedHandler at your HttpSecurity in you Security Config.
Below Simple way de return JSON For 403 error :
Define a private method in Config Security like this :
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
.....
private void writeResponse(HttpServletResponse httpResponse, int code, String message) throws IOException {
httpResponse.setContentType("application/json");
httpResponse.setStatus(code);
httpResponse.getOutputStream().write(("{\"code\":" + code + ",").getBytes());
httpResponse.getOutputStream().write(("\"message\":\"" + message + "\"}").getBytes());
httpResponse.getOutputStream().flush();
}
}
Add exceptionHandling at your HttpSecurity Config :
#Override
protected void configure(HttpSecurity http) throws Exception {
....
http = http.exceptionHandling()
.accessDeniedHandler((request, response, accessDeniedException) -> {
this.writeResponse(response,HttpServletResponse.SC_FORBIDDEN,accessDeniedException.getMessage());
}
)
.and();
....
}

How to call controller method after authentication

I have following metod in controller:
#PostMapping(path = "/api/users/login", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
#ResponseStatus(OK)
public TokenResponse login(#RequestBody LoginUserRequest loginUserRequest, Principal principal) {
return new TokenResponse().setAccessToken("token");
}
here is a WebSecurityConfigurerAdapter
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin().disable()
.authorizeRequests().antMatchers("/api/users/login").permitAll()
.and()
.authorizeRequests().antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(mobileAuthenticationFilter(objectMapper), UsernamePasswordAuthenticationFilter.class)
.addFilter(new JwtAuthorizationFilter(authenticationManager(), super.userDetailsService()));
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("SELECT login, pass, active FROM users WHERE login = ?")
.authoritiesByUsernameQuery("SELECT login, 'ROLE_USER' FROM users WHERE login = ?")
.passwordEncoder(new CustomPasswordEncoder());
}
#Bean
public MobileAuthenticationFilter mobileAuthenticationFilter(ObjectMapper objectMapper) throws Exception {
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter(objectMapper);
mobileAuthenticationFilter.setAuthenticationManager(authenticationManager());
mobileAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
System.out.println(request);
});
return mobileAuthenticationFilter;
}
MobileAuthenticationFilter is reading from json body and prepare UsernamePasswordAuthenticationToken
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper;
public MobileAuthenticationFilter(ObjectMapper objectMapper) {
super(new AntPathRequestMatcher("/api/users/login"));
this.objectMapper = objectMapper;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
BufferedReader reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.mark(0);
LoginUserRequest loginUserRequest = objectMapper.readValue(sb.toString(), LoginUserRequest.class);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginUserRequest.getLogin(), loginUserRequest.getPassword());
return getAuthenticationManager().authenticate(token);
} catch (IOException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
}
this code works fine but is one thing which I want to archive.
After successfully authentication, response is produced immediately by the:
mobileAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
System.out.println(request);
});
Here ofcourse I can return something to client (in body), but there is any possibility to invoke controller method public TokenResponse login and that method should return a response (based on method contract and annotations for http code)?
This method in controller in that scenario is never called.
Would there be a formLogin, you could have used the successHandler(...) to redirect to the page you want. Note that you have to also think about error responses.
Since you have explicitly disabled formLogin, I recommend if users call /api/users/login instead of authenticating them in attemptAuthentication(...).
So, as you have put it ..addFilterBefore(mobileAuthenticationFilter(objectMapper), UsernamePasswordAuthenticationFilter.class), your filter will be triggered populating the resulting response.
Your controller will look like something like this:
public TokenResponse login(#Valid #RequestBody LoginUserRequest loginUserRequest) {
//may be check for AuthenticationException
try{
...
generateToken(loginUserRequest.getUserName(), loginUserRequest.getPassword());
...
} catch(AuthenticationException ex){
// status = HttpStatus.UNAUTHORIZED;
} catch (Exception ex) {
//status = HttpStatus.INTERNAL_SERVER_ERROR;
}
}
public String generateToken(String name, String password) {
try {
// check for null or empty
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(name, password, new ArrayList<>());
Authentication authentication = authenticationManager.authenticate(upToken);
// do whatever operations you need and return token
} catch (Exception ex) {
throw ex;
}
}

IP Address verification in REST API with SpringSecurity

I have Spring Boot application with configured SpringSecurity. It uses token generated by UUID.randomUUID().toString(), returned by method login in UUIDAuthenticationService class in AuthUser object. Authorized users are kept in LoggedInUsers class. When I'm sending request to API token is verified by method findByToken in UUIDAuthenticationService class.
Lastly I added timeout for token verification. Now I want to add ip address verification. If user is logged in from address X.X.X.X (which is kept in AuthUser object) he should be authorized with his token only form address X.X.X.X. How to do it?
My SecurityConfig.java:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#FieldDefaults(level = PRIVATE, makeFinal = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/api/login/login"),
);
private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
TokenAuthenticationProvider provider;
SecurityConfig(final TokenAuthenticationProvider provider) {
super();
this.provider = requireNonNull(provider);
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.requestMatchers(PUBLIC_URLS);
web.httpFirewall(defaultHttpFirewall());
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet
// authenticated
.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.and()
.authenticationProvider(provider)
.addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN")
.antMatchers("/api/application/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_EMPLOYEE", "ROLE_PORTAL")
.antMatchers("/api/rezerwacja/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_EMPLOYEE")
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
}
#Bean
TokenAuthenticationFilter restAuthenticationFilter() throws Exception {
final TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
#Bean
SimpleUrlAuthenticationSuccessHandler successHandler() {
final SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setRedirectStrategy(new NoRedirectStrategy());
return successHandler;
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(final TokenAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
#Bean
public HttpFirewall defaultHttpFirewall() {
return new DefaultHttpFirewall();
}
}
AbstractAuthenticationProcessingFilter.java:
#FieldDefaults(level = PRIVATE, makeFinal = true)
public final class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String BEARER = "Bearer";
public TokenAuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
#Override
public Authentication attemptAuthentication(
final HttpServletRequest request,
final HttpServletResponse response) {
final String param = ofNullable(request.getHeader(AUTHORIZATION))
.orElse(request.getParameter("t"));
final String token = ofNullable(param)
.map(value -> removeStart(value, BEARER))
.map(String::trim)
.orElseThrow(() -> new BadCredentialsException("Missing Authentication Token"));
final Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
#Override
protected void successfulAuthentication(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain,
final Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
TokenAuthenticationProvider/java:
#Component
#AllArgsConstructor(access = PACKAGE)
#FieldDefaults(level = PRIVATE, makeFinal = true)
public final class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#NonNull
UserAuthenticationService auth;
#Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
#Override
protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
final Object token = authentication.getCredentials();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(auth::findByToken)
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
}
UUIDAuthenticationService.java:
#Service
#AllArgsConstructor(access = PACKAGE)
#FieldDefaults(level = PRIVATE, makeFinal = true)
public final class UUIDAuthenticationService implements UserAuthenticationService {
private static final Logger log = LoggerFactory.getLogger(UUIDAuthenticationService.class);
#NonNull
UserCrudService users;
#Autowired
LoginManager loginMgr;
#Override
public AuthUser login(final String username, final String password) throws Exception { //throws Exception {
AuthUser user = loginMgr.loginUser(username, password);
if (user != null) {
users.delete(user);
users.save(user);
log.info("Zalogowano użytkownika {}, przydzielono token: {}", user.getUsername(), user.getUuid());
}
return Optional
.ofNullable(user)
.orElseThrow(() -> new RuntimeException("Błędny login lub hasło"));
}
#Override
public Optional<AuthUser> findByToken(final String token) {
AuthUser user = users.find(token).orElse(null); // get();
if (user != null) {
Date now = Date.from(OffsetDateTime.now(ZoneOffset.UTC).toInstant());
int ileSekund = Math.round((now.getTime() - user.getLastAccess().getTime()) / 1000); // timeout dla tokena
if (ileSekund > finals.tokenTimeout) {
log.info("Token {} dla użytkownika {} przekroczył timeout", user.getUuid(), user.getUsername());
users.delete(user);
user = null;
}
else {
user.ping();
}
}
return Optional.ofNullable(user); //users.find(token);
}
#Override
public void logout(final AuthUser user) {
users.delete(user);
}
}
I thought about creating method findByTokenAndIp in UUIDAuthenticationService, but I don't know how to find ip address of user sending request and how to get ip address while logging in login method in UUIDAuthenticationService (I need it while I'm creating AuthUser object).
You had access to HttpServletRequest request in your filter so you can extract the IP from it.
See https://www.mkyong.com/java/how-to-get-client-ip-address-in-java/
After having the IP, you can deny the request anyway that you want!
I would briefly do the following steps:
save the IP in the UUIDAuthenticationService. You can add HttpServletRequest request as a param, if you're using a controller/requestmapping, because it's auto-injected:
#RequestMapping("/login")
public void lgin(#RequestBody Credentials cred, HttpServletRequest request){
String ip = request.getRemoteAddr();
//...
}
Within the authentication filter, use the IP as the "username" for the UsernamePasswordAuthenticationToken and the token as the "password". There is also already the HttpServletRequest request that gives you the IP by getRemoteAddr().
It's also possible to create an own instance of AbstractAuthenticationToken or even UsernamePasswordAuthenticationToken, which explictly holds an IP or even the request for the authentication-manager.
Then, you just need to adapt the changes to your retrieveUser method.
I modified controller to get ip address with HttpServletRequest parameter and add parameter ipAddress to login method.
#PostMapping("/login")
public AuthUser login(InputStream inputStream, HttpServletRequest request) throws Exception {
final String ipAddress = request.getRemoteAddr();
if (ipAddress == null || ipAddress.equals("")) {
throw new Exception("Nie udało się ustalić adresu IP klienta");
}
Login login = loginMgr.prepareLogin(inputStream);
return authentication
.login(login.getUsername(), login.getPasword(), ipAddress);
}
And modified method retrieveUser in TokenAuthenticationProvider
#Override
protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
System.out.println("Verification: "+authentication.getPrincipal()+" => "+authentication.getCredentials());
final Object token = authentication.getCredentials();
final String ipAddress= Optional
.ofNullable(authentication.getPrincipal())
.map(String::valueOf)
.orElse("");
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(auth::findByToken)
.filter(user -> user.ipAddress.equals(ipAddress)) // weryfikacja adresu ip
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
And it works. Thans for help.

How to make Shiro return 403 Forbidden with Spring Boot rather than redirect to login.jsp

I have a server that is just an API endpoint, no client front-end, no jsp, no html. It uses Spring Boot and I'm trying to secure it with Shiro. The relevent parts of my SpringBootServletInitializer look like this. I'm trying to get Shiro to return a 403 response if it fails the roles lookup as defined in BasicRealm. Yet it seems to default to redirecting to a non-existent login.jsp and no matter what solution I seem to use. I can't override that. Any help would be greatly appreciated.
#SpringBootApplication
public class RestApplication extends SpringBootServletInitializer {
...
#Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> filterChain = new HashMap<>();
filterChain.put("/admin/**", "roles[admin]");
shiroFilter.setFilterChainDefinitionMap(filterChain);
shiroFilter.setSecurityManager(securityManager());
return shiroFilter;
}
#Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
CookieRememberMeManager rmm = new CookieRememberMeManager();
rmm.setCipherKey(Base64.decode("XXXXXXXXXXXXXXXXXXXXXX"));
securityManager.setRememberMeManager(rmm);
return securityManager;
}
#Bean(name = "userRealm")
#DependsOn("lifecycleBeanPostProcessor")
public BasicRealm userRealm() {
return new BasicRealm();
}
#Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
public class BasicRealm extends AuthorizingRealm {
private static Logger logger = UserService.logger;
private static final String REALM_NAME = "BASIC";
public BasicRealm() {
super();
}
#Override
protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String userid = upToken.getUsername();
User user = Global.INST.getUserService().getUserById(userid);
if (user == null) {
throw new UnknownAccountException("No account found for user [" + userid + "]");
}
return new SimpleAuthenticationInfo(userid, user.getHashedPass().toCharArray(), REALM_NAME);
}
#Override
protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
String userid = (String) principals.getPrimaryPrincipal();
if (userid == null) {
return new SimpleAuthorizationInfo();
}
return new SimpleAuthorizationInfo(Global.INST.getUserService().getRoles(userid));
}
}
OK, here is how I solved it. I created a class ...
public class AuthFilter extends RolesAuthorizationFilter {
private static final String MESSAGE = "Access denied.";
#Override
protected boolean onAccessDenied(final ServletRequest request, final ServletResponse response) throws IOException {
HttpServletResponse httpResponse ;
try {
httpResponse = WebUtils.toHttp(response);
}
catch (ClassCastException ex) {
// Not a HTTP Servlet operation
return super.onAccessDenied(request, response) ;
}
if (MESSAGE == null) {
httpResponse.sendError(403);
} else {
httpResponse.sendError(403, MESSAGE);
}
return false; // No further processing.
}
}
... and then in my shiroFilter() method above I added this code ...
Map<String, Filter> filters = new HashMap<>();
filters.put("roles", new AuthFilter());
shiroFilter.setFilters(filters);
... hope this helps someone else.
In Shiro 1.4+ you can set the login url in your application.properties:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web/src/main/resources/application.properties#L20
Earlier versions you should be able to set ShiroFilterFactoryBean.setLoginUrl("/login")
https://shiro.apache.org/static/current/apidocs/org/apache/shiro/spring/web/ShiroFilterFactoryBean.html

Categories