Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring security- Send credentials as json instead of regular form in rest service

I am writing rest service with json. For backend I use Spring Security. I have form witch sends with ajax rest object as follow:

{email: "admin", password: "secret"}

Now on the server I have configuration as follow:

@Configuration
@EnableWebSecurity
@ComponentScan("pl.korbeldaniel.cms.server")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private RestAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private RestAuthenticationFailureHandler authenticationFailureHandler;

    @Bean
    JsonAuthenticationFilter jsonAuthenticationFilter() throws Exception {
    JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    System.out.println("jsonAuthenticationFilter");
    return filter;
    }

    @Bean
    public RestAuthenticationSuccessHandler mySuccessHandler() {
    return new RestAuthenticationSuccessHandler();
    }

    @Override
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("admin").password("secret").roles("ADMIN");
    // auth.jdbcAuthentication().
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    http.csrf().disable();//
    http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint)//
        .and().authorizeRequests()//
        .antMatchers("/").permitAll()//
        .antMatchers("/services/anonymous/**").permitAll()//
        .antMatchers("/services/authenticated/**").authenticated()//
        .and().formLogin().loginProcessingUrl("/services/anonymous/loginService/login").usernameParameter("email").passwordParameter("password")//
        .successHandler(authenticationSuccessHandler)//
        .and().logout().logoutUrl("/services/anonymous/loginService/logout");
    // http.httpBasic();
    }
}

Problem is that spring security demands from me to send credentials as body, but I would like to spring accept my Json object.

So I've wrote my own authentication filter base on this:

@Component
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private boolean postOnly;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    System.out.println("attemptAuthentication");
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }

    UsernamePasswordAuthenticationToken authRequest = this.getUserNamePasswordAuthenticationToken(request);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * @param request
     * @return
     */
    private UsernamePasswordAuthenticationToken getUserNamePasswordAuthenticationToken(HttpServletRequest request) {
    // TODO Auto-generated method stub
    System.out.println(request);
    return null;
    }

}

But unfortunatelly this filter seems to not work.

When I send ajax post request from login form, I am getting 302 Found and then I am getting this:

Remote Address:127.0.0.1:8080
Request URL:http://localhost:8080/cms/login?error
Request Method:GET
Status Code:404 Not Found

Like there fail to validate user credential (cause form body is empty and credentials goes as json), and then it redirect to login?error which doesn't exist cause I've my own login form.

Please help.


Edit

public class WebServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { SecurityConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
    // return new String[] { "/" };
    // return new String[] { "/cms/" };
    return new String[] { "/services/*" };
    }
}


@EnableWebMvc
@ComponentScan(basePackages = "pl.daniel.cms.server")
public class WebConfig extends WebMvcConfigurerAdapter {
}
like image 741
masterdany88 Avatar asked Mar 01 '16 13:03

masterdany88


People also ask

Does Spring Security use default login form?

In this configuration Spring Security will render a default log in page. Most production applications will require a custom log in form. The configuration below demonstrates how to provide a custom log in form. public SecurityFilterChain filterChain(HttpSecurity http) { http .

Is WebSecurityConfigurerAdapter deprecated?

From Spring Boot 2.7, WebSecurityConfigurerAdapter is deprecated.


2 Answers

Well, it must not work until you write the getUserNamePasswordAuthenticationToken body.

Actually, you must read the request body of the HttpServletRequest, parse it through Jackson or any other mapping way and create with it the UsernamePasswordAuthenticationToken.

Using Jackson (choose the right version depending on your Spring version), I would create a simple bean like this:

@JsonIgnoreProperties(ignoreUnkown=true)
    public LoginRequest{
     private String email;
     private String password;
     // getters & setters
    }

The use it to map it the request body:

private UsernamePasswordAuthenticationToken getUserNamePasswordAuthenticationToken(HttpServletRequest request)  throws IOException{
    StringBuffer sb = new StringBuffer();
    BufferedReader bufferedReader = null;
    String content = "";
    LoginRequest sr = null;

    try {
        bufferedReader =  request.getReader()
        char[] charBuffer = new char[128];
        int bytesRead;
        while ( (bytesRead = bufferedReader.read(charBuffer)) != -1 ) {
            sb.append(charBuffer, 0, bytesRead);
        }
        content = sb.toString();
        ObjectMapper objectMapper = new ObjectMapper();
        try{
            sr = objectMapper.readValue(content, LoginRequest.class);
        }catch(Throwable t){
            throw new IOException(t.getMessage(), t);
        }
    } catch (IOException ex) {

        throw ex;
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException ex) {
                throw ex;
            }
        }
    }
    return new UsernamePasswordAuthenticationToken(sr.getEmail(), sr.getPassword());

}

P.D. Yo must use Post, you will never be able to post a request-body using GET

like image 110
jlumietu Avatar answered Sep 24 '22 01:09

jlumietu


You can extend and override WebSecurityConfigurerAdapter

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .addFilter(new UserNamePasswordAuthFilter(authenticationManager(), userRepo))
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .httpBasic();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider());
    }
    
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService); // custom user service
        provider.setPasswordEncoder(passwordEncoder); // custom password encoder
        return provider;
    }

Then you can define a filter for authentication and optionally you can override successful login behavior.

public class UserNamePasswordAuthFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authManager;
    private final AecUserRepo userRepo;
    
    public UserNamePasswordAuthFilter(AuthenticationManager authManager, AecUserRepo userRepo) {
        super();
        this.authManager = authManager;
        this.userRepo = userRepo;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        try {
            // Get username & password from request (JSON) any way you like
            UsernamePassword authRequest = new ObjectMapper()
                    .readValue(request.getInputStream(), UsernamePassword.class);
            
            Authentication auth = new UsernamePasswordAuthenticationToken(authRequest.getUsername(), 
                    authRequest.getPassword());
            
            return authManager.authenticate(auth);
        } catch (Exception exp) {
            throw new RuntimeException(exp);
        }
    }
    
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        // custom code
        
        SecurityContextHolder.getContext().setAuthentication(authResult);       
    }
}
like image 44
quintin Avatar answered Sep 26 '22 01:09

quintin