spring boot + security + jquery ajax - ajax

Hi,I build a project with spring boot & spring security. Now, I want to provider the login restfull service for Jquery.ajax({...}); And I want to:
process the login request from HTML page (like the <form> submit).
automatic to check session timeout when HTML page request, redirect timeout to login page.
process the login request from Ajax.
automatic to check the login state when Ajax request.
I coding like this
SecurityConfig
extends from WebSecurityConfigurerAdapter
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
String requestType = request.getHeader("x-requested-with");
if (!StringUtils.isEmpty(requestType)) {
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print("{\"invalid_session\": true}");
response.getWriter().flush();
} else {
response.sendRedirect("/security/login");
}
});
http.authorizeRequests()
.antMatchers("/security/**").permitAll()
.antMatchers("/reader/**").hasRole("READER")
.anyRequest().authenticated()
// session time out
.and().sessionManagement().invalidSessionUrl("/security/session_timeout")
.and().cors()
// login
.and()
.formLogin()
.successHandler(successHandler)
.failureHandler(faildHandler)
.loginPage("/security/login")
.permitAll()
// logout
.and()
.logout().permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(readerRepository::findOne);
}
I have two handlers to process AuthenticationSuccess and AuthenticationFailure.
FailureHandler
extends from SimpleUrlAuthenticationFailureHandler
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("Failed to auth.");
String requestType = request.getHeader("x-requested-with");
if (!StringUtils.isEmpty(requestType)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print("{\"success\": false}");
} else {
setDefaultFailureUrl("/security/login?error=true");
super.onAuthenticationFailure(request, response, exception);
}
}
SuccessHandler
extends from SavedRequestAwareAuthenticationSuccessHandler
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("Success to auth.");
String requestType = request.getHeader("x-requested-with");
if (!StringUtils.isEmpty(requestType)) {
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print("{\"success\": true}");
} else {
setDefaultTargetUrl("/index/index");
setAlwaysUseDefaultTargetUrl(true);
super.onAuthenticationSuccess(request, response, authentication);
}
}
Controller
base RequestMapping is '/security'
#RequestMapping(value = "/login")
public String login(#RequestParam(value = "error", defaultValue = "false") boolean error, Model model) {
model.addAttribute("error", error);
return "login";
}
#RequestMapping("/session_timeout")
public void sessionTimeout(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("session was timeout.");
if (request.getHeader("x-requested-with") != null) {
// handler for ajax
response.getWriter().print("{\"sessionTimeout\": true}");
response.getWriter().close();
} else {
response.sendRedirect("login");
}
}
When I test in page(thymeleaf), All of worked.
but.. when I use Jquery Ajax.
The issure:
When I used Jquery.ajax({}) API to send the request, the request can not be get to server. How to write ajax request with jquery, I tried a lot of Jquery methods, page has no response code in console. Is the spring security not support ajax?

Thanks Fan, I fixed it. I rewrote the login authentication:
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, userReq.getPassword(), userDetails.getAuthorities());
authenticationManager.authenticate(token);
if (token.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(token);
return true;
}
Do auth with AuthenticationManager and it injected from spring.
If success to auth, I will return the sessionid to the client and client saved in cookie, when client do request, the client always stats sessionid at the end of the url requested by ajax.
If failed to auth, I wile return agreed error code.
eg:
$.ajax({
url: 'http://test:port/project/list;jsessionid=' + jessionid,
...
})
But I do not think so is good job like this. It's very troublesome, In the client,I need to check every response code is it right or not for every request. Is any better ways to solve this ensure??
by the way, the Client(Browser + Ajax) and Server(Spring mvc) are separate.

Related

How to add a header on an auth redirect response with Spring?

For integration of Spring Boot with htmx, I need a way to add a header if an incoming request is done by htmx and the user is no longer logged on.
In the normal flow, the user gets redirected to the login page. However, when there is a request done by htmx, this is an AJAX request and the redirect is not happening.
The recommended solution is that if there is an HX-Request header on the request, that the server should put an HX-Refresh: true header on the response. This will make htmx do a full page refresh.
My security config looks like this:
#Configuration
public class WebSecurityConfiguration {
private final ClientRegistrationRepository clientRegistrationRepository;
public WebSecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(registry -> {
registry.mvcMatchers("/actuator/info", "/actuator/health").permitAll();
registry.mvcMatchers("/**").hasAuthority(Roles.ADMIN);
registry.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
registry.anyRequest().authenticated();
});
http.oauth2Client();
http.oauth2Login();
http.logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
return http.build();
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return logoutSuccessHandler;
}
}
I tried with a Filter:
public Filter htmxFilter() {
return new Filter() {
#Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
filterChain.doFilter(servletRequest, servletResponse);
String htmxRequestHeader = request.getHeader("HX-Request");
System.out.println("htmxRequestHeader = " + htmxRequestHeader);
System.out.println(response.getStatus());
if (htmxRequestHeader != null
&& response.getStatus() == 302) {
System.out.println("XXXXXXXXXXX");
response.setHeader("HX-Refresh", "true");
}
}
};
}
But response.getStatus() is never 302 (altough I can see the 302 response status in Chrome).
I also tried with an Interceptor:
#Bean
public HandlerInterceptor htmxHandlerInterceptor() {
return new HandlerInterceptor() {
#Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
boolean htmxRequest = request.getHeader("HX-Request") != null;
String htmxRequestHeader = request.getHeader("HX-Request");
System.out.println("htmxRequestHeader = " + htmxRequestHeader);
System.out.println(response.getStatus());
if( htmxRequest && response.getStatus() == 302) {
response.setHeader("HX-Refresh", "true");
}
}
};
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeInterceptor());
registry.addInterceptor(htmxHandlerInterceptor());//.order(Ordered.HIGHEST_PRECEDENCE);
}
Which also does not work, there is no 302 response status.
I also tried with the commented out order(Ordered.HIGHEST_PRECEDENCE), but that did not make any difference.
Are there other options?
When a request comes to a protected endpoint and it is not authenticated, Spring Security executes its AuthenticationEntryPoints interface to commence an authentication scheme.
You could create your own AuthenticationEntryPoint that adds the header and delegates to the LoginUrlAuthenticationEntryPoint (or other implementation that you are using).
#Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
//...
.exceptionHandling(exception -> exception
.authenticationEntryPoint(new HxRefreshHeaderAuthenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
);
return http.build();
}
public class HxRefreshHeaderAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final AuthenticationEntryPoint delegate;
public HxRefreshHeaderAuthenticationEntryPoint(AuthenticationEntryPoint delegate) {
this.delegate = delegate;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Add the header
this.delegate.commence(request, response, authException);
}
}
You need to make sure that your filter runs before any Spring Security filter. See at SecurityProperties.DEFAULT_FILTER_ORDER or HttpSecurity#addFilterBefore

Redirect to original URL after successful authentication in Spring Security

I have the following security configuration class in a Spring Cloud Gateway application. This gateway acts as an OAuth2 client handling the user authentication. After a successful authentication, I'd like to redirect to the URL of the single-page application where the user originally came from.
Example
If the user was on http://localhost:8093/profile then this should be the redirect URL.
Currently I only use a hardcoded value which works for testing purposes. Is there a way to get the "original URL" and use it as a redirection URL?
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
httpSecurity
.csrf().disable()
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2Login()
// Use original URL here?
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("http://localhost:8093"))
.and()
.exceptionHandling().authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.oauth2ResourceServer().jwt();
return httpSecurity.build();
}
}
You can try below provide the combination to Achieve what you are looking for:
First of all you need to create your Authentication Success Handler:
public class MySimpleUrlAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
protected Log logger = LogFactory.getLog(this.getClass());
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
Then handle Method implementation:
protected void handle(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
//This will provide you last URL
String targetUrl = request.getHeader("referer");
if (response.isCommitted()) {
logger.debug(
"Response has already been committed. Unable to redirect to "
+ targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
Just an FYI:
Note: the HTTP referer is a client-controlled value and can thus be spoofed to something entirely different or even removed. This value should not be used for any critical operation.
Maybe it's too late, but I had the same problem like you. Has Jayesh said, you need to create a class "Authentication Success Handler" to add some logic and redirection after a sucessfull Oauth2 authentication.
But this new class , instead of extending SimpleUrlAuthenticationSucessHandler, must extends SavedRequestAwareAuthenticationSucessHandler and override the method onAuthenticationSucess().
public class OAuth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
#Autowired
private UserService userService;
#Autowired
private MessageSource messageSource;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
User existingUser = userService.findByUsername(oAuth2User.getEmail());
if (existingUser != null) {
// update of user with providerId and authenticationProvider if not already done
log.info(messageSource.getMessage("global.existing-user.oauth2-authenticated",
new Object[] { existingUser }, LocaleContextHolder.getLocale()));
if (existingUser.getAuthenticationProvider() == AuthProvider.LOCAL) {
userService.updateUserFromOAuth2Authentication(oAuth2User, existingUser);
} else if ((!Objects.equals(existingUser.getIdProvider(), oAuth2User.getproviderId())
|| existingUser.getAuthenticationProvider() != oAuth2User.getClientProvider())) {
throw new OAuth2AuthenticationException("a problem occured with Oauth2Authentication!");
}
} else {
// creation of new user
log.info(messageSource.getMessage("global.not-existing-user.oauth2-authenticated",
new Object[] { "createdUser" }, LocaleContextHolder.getLocale()));
userService.saveUserFromOAuth2Authentication(oAuth2User);
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
In your configuration class for security, you just have to call the method successHandler()for Oauth2Login to use your new class "authentication success handler" without of course, using method defaultSucessUrl() , like this
http.oauth2Login()
.loginPage("/app/login")
.userInfoEndpoint()
.userService(oauth2UserService)
.and()
.successHandler(oAuth2LoginSuccessHandler)
;
Sorry for my bad english, i found this solution just after reading this article https://www.baeldung.com/spring-security-redirect-login

How to do basic authentication using cookies in spring security?

I am securing my REST api using Basic-Auth. On correct credentials passed by user, a controller is responsible for sending a httpOnly and secure cookie in response.
#GetMapping
#ResponseStatus(value=HttpStatus.OK)
public void loginUser( final HttpServletRequest request ,final HttpServletResponse response) throws UnsupportedEncodingException {
setAuthCookieToResonse(request,response);
}
private void setAuthCookieToResonse(final HttpServletRequest request ,final HttpServletResponse response) throws UnsupportedEncodingException {
String cookieKey = "auth";
String cookieValue = request.getHeader("Authorization");
if (cookieValue != null) {
Cookie cookie = new Cookie(cookieKey, cookieValue);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
}
}
So, now with each request a cookie is being sent by the browser, which will contain Basic-Auth details. But the problem is, how do the spring security extract those credentials from that cookie?
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {// #formatter:off
httpSecurity
.cors()
.and().authorizeRequests()
.antMatchers("/signup/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
;
}
My guess was:
To add a filter before BasicAuthenticationFilter.class and extract the credentials from cookie and than add those credentials to the HttpServletRequest's Authorizaton header which is going to be passed to spring-security layer. But the problem is, HttpServletRequest doesn't have API to add headers.
What would be the right way to implement this?
I made this working after following this blog (archived). But I would love to hear other solutions, especially using some spring configuration itself. Spring is a very matured framework, it must(should) have something to handle this common problem.
Since, the HttpServletRequest don't have any method to add the new headers, I need to create a custom class which can add new headers to the request, this can be achived by HttpServletRequestWrapper. Here is the implementation.
public final class MutableHttpServletRequest extends HttpServletRequestWrapper {
// holds custom header and value mapping
private final Map<String, String> customHeaders;
public MutableHttpServletRequest(HttpServletRequest request) {
super(request);
this.customHeaders = new HashMap<String, String>();
}
public void putHeader(String name, String value) {
this.customHeaders.put(name, value);
}
public String getHeader(String name) {
// check the custom headers first
String headerValue = customHeaders.get(name);
if (headerValue != null) {
return headerValue;
}
// else return from into the original wrapped object
return ((HttpServletRequest) getRequest()).getHeader(name);
}
public Enumeration<String> getHeaderNames() {
// create a set of the custom header names
Set<String> set = new HashSet<String>(customHeaders.keySet());
// now add the headers from the wrapped request object
Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
while (e.hasMoreElements()) {
// add the names of the request headers into the list
String n = e.nextElement();
set.add(n);
}
// create an enumeration from the set and return
return Collections.enumeration(set);
}
}
The filter which checks for the cookie, before the Spring-secuirty:
public class CheckAuthCookieFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(getClass());
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
MutableHttpServletRequest mutableRequest = new MutableHttpServletRequest(httpServletRequest);
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
logger.debug(cookie.getName() + " : " + cookie.getValue());
if (cookie.getName().equalsIgnoreCase("auth")) {
mutableRequest.putHeader("Authorization", URLDecoder.decode(cookie.getValue(), "utf-8"));
}
}
}
chain.doFilter(mutableRequest, response);
}
}
and finally the security configuration:
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {// #formatter:off
httpSecurity
.cors()
.and().authorizeRequests()
.antMatchers("/signup/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
;
httpSecurity.addFilterBefore(new CheckAuthCookieFilter(), BasicAuthenticationFilter.class);
}
My custom filter will run before the Spring's BasicAuthenticationFilter.If there is a cookie present with name auth(which the application created on successful login), than that's the cookie which holds the basic auth credentials. The credentials are extracted from that, and added to the header of request. Then the BasicAuthenticationFilter will run and look for the Authorization and proceed with its normal flow.

SpringMVC with rest controller - Request method 'POST' not supported

I have application in SpringMVC with Spring Security. In spring security xml configuration file i enable csrf using <csrf/>. View in html work properly when i add csrf token to link in form but now i want add Controller and made some rest communication. Controller looks like this:
#Controller
public class RestControllerTest {
#RequestMapping(value = "/rest", method = RequestMethod.POST, produces = "application/json")
#ResponseStatus(value = HttpStatus.OK)
public #ResponseBody void setUser(#RequestBody User user){
System.out.println("Witaj " + user.getFirstname() + "!");
}
}
When i try to send JSon with user (using postman):
{
"firstname": "name",
"lastname": "name"
}
I get 404 not found status in postman and WARN : org.springframework.web.servlet.PageNotFound - Request method 'POST' not supported in STS. I know that I have to use csrf token but i don't have idea how to do this.
I have a similar working usecase as yours. This is how things look like in my case.
My Filter class -
public class MyCsrfFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws Exception {
CsrfToken csrf = (CsrfToken) request.getAttribute(org.springframework.security.web.csrf.CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath(YOUR_DOMAIN_NAME);
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}
Below is the security configuration in spring-boot. You can convert it to equivalent xml config file if you need one.
#Configuration
#EnableWebMvcSecurity
public class CustomWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.csrfTokenRepository(getTokenRepo())
.and()
.authorizeRequests()
.antMatchers("/login", "/login**").permitAll()
.and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);
}
private CsrfTokenRepository getTokenRepo() {
HttpSessionCsrfTokenRepository repo = new HttpSessionCsrfTokenRepository();
repo.setHeaderName("X-XSRF-TOKEN");
return repo;
}
Hope this helps !

Spring Cloud Single Sign On but only if already logged in

I've got a spring cloud oauth2 server up and running with jwt and a client which config looks like this
#EnableOAuth2Sso
public class PortalApplication {
public static void main(String[] args) {
SpringApplication.run(PortalApplication.class, args);
}
#Component
public static class LoginConfigurer extends OAuth2SsoConfigurerAdapter {
#Override
public void match(RequestMatchers matchers) {
matchers.antMatchers("/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = new Cookie("XSRF-TOKEN", csrf.getToken());
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
}
This is all fine and the app redirects to the auth server and redirects back to the Portal app when done.
Client number two
But now I've got another client which is a web shop.
If the user is logged in at the auth server then the web shop should display a bar in the top with links to the users previous orders and a profile.
If the user is NOT logged in, then nothing should happen until the checkout step where they will have to either login or create an account.
Question
How can I auto login if I'm already logged in to the auth server, but allow anonymous users otherwise?
Can I somehow use the spring.oauth2.resource.userInfoUri to achieve this?

Resources