3

I use this code for Rest API authentication:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        Optional<String> basicToken = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
                .filter(v -> v.startsWith("Basic"))
                .map(v -> v.split("\\s+")).filter(a -> a.length == 2).map(a -> a[1]);
        if (!basicToken.isPresent()) {
            return sendAuthError(response);
        }

        byte[] bytes = Base64Utils.decodeFromString(basicToken.get());
        String namePassword = new String(bytes, StandardCharsets.UTF_8);
        int i = namePassword.indexOf(':');
        if (i < 0) {
            return sendAuthError(response);
        }
        String name = namePassword.substring(0, i);
        String password = namePassword.substring(i + 1);
//        Optional<String> clientId = authenticationService.authenticate(name, password, request.getRemoteAddr());
        Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
        if (merchant == null) {
            return sendAuthError(response);
        }
        request.setAttribute(CURRENT_CLIENT_ID_ATTRIBUTE, merchant.getId());
        return true;
    }

How I can rewrite the code with Spring Security in order to get the same result but for different links to have authentication? For example:

localhost:8080/v1/notification - requests should NOT be authenticated.
localhost:8080/v1/request - requests should be authenticated.
Peter Penzov
  • 1,126
  • 134
  • 430
  • 808
  • Well you are uing a basic authentication so you may use something similar to https://www.baeldung.com/spring-security-basic-authentication. In any case for REST API I strongly suggest to you to use OAuth2 and JWT – Angelo Immediata Sep 04 '18 at 07:52
  • @AngeloImmediata can you paste official answer with working code, please? – Peter Penzov Sep 04 '18 at 07:53

2 Answers2

5

Here you can find a working project https://github.com/angeloimm/springbasicauth

I know in the pom.xml file there are a lot of useless dependencies but I started from an already existing project and I had no time to depure it

Basically you must:

  • configure spring security
  • configure spring mvc
  • implements your own authentication provider according to spring security. Note I used an inMemoryAuthentication. Please modify it according to yuor own wishes

Let me explain the code.

Spring MVC Configuration:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages= {"it.olegna.test.basic"})
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

Here we don't do anything else that configuring spring MVC by telling it where to find controllers and so on and to use a single message converter; the MappingJackson2HttpMessageConverter in order to produce JSON responses

Spring Security Configuration:

@Configuration
@EnableWebSecurity
@Import(value= {WebMvcConfig.class})
public class WebSecConfig extends WebSecurityConfigurerAdapter {
     @Autowired private RestAuthEntryPoint authenticationEntryPoint;

        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth
              .inMemoryAuthentication()
              .withUser("test")
              .password(passwordEncoder().encode("testpwd"))
              .authorities("ROLE_USER");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
              .authorizeRequests()
              .antMatchers("/securityNone")
              .permitAll()
              .anyRequest()
              .authenticated()
              .and()
              .httpBasic()
              .authenticationEntryPoint(authenticationEntryPoint);
        }
        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
}

Here we configure Spring Security in order to use HTTP Basic Authentication for all requests except the ones starting with securityNone. We use a NoOpPasswordEncoder in order to encode the provided password; this PasswrodEncoder does absolutly nothing... it leaves the passwrod as it is.

RestEntryPoint:

@Component
public class RestAuthEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,  AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

This entrypoint disables all requests not containg the Authentication header

SimpleDto: a very simple DTO representing the JSON answer form a controller

public class SimpleDto implements Serializable {

    private static final long serialVersionUID = 1616554176392794288L;
    private String simpleDtoName;

    public SimpleDto() {
        super();
    }
    public SimpleDto(String simpleDtoName) {
        super();
        this.simpleDtoName = simpleDtoName;
    }
    public String getSimpleDtoName() {
        return simpleDtoName;
    }
    public void setSimpleDtoName(String simpleDtoName) {
        this.simpleDtoName = simpleDtoName;
    }

}

TestBasicController: a very simple controller

@RestController
@RequestMapping(value= {"/rest"})
public class TestBasicController {
    @RequestMapping(value= {"/simple"}, method= {RequestMethod.GET}, produces= {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public ResponseEntity<List<SimpleDto>> getSimpleAnswer()
    {
        List<SimpleDto> payload = new ArrayList<>();
        for(int i= 0; i < 5; i++)
        {
            payload.add(new SimpleDto(UUID.randomUUID().toString()));
        }
        return ResponseEntity.ok().body(payload);
    }
}

So if you try this project by using postman or any other tester you can have 2 scenarios:

  • authentication required
  • all ok

Let's suppose you want to invoke the URL http://localhost:8080/test_basic/rest/simple without passing the Authentication header. The HTTP Status code will be 401 Unauthorized

This means that the Authentication Header is required

By adding this header to the request Authorization Basic dGVzdDp0ZXN0cHdk all works pretty good Note that the String dGVzdDp0ZXN0cHdk is the Base64 encoding of the string username:password; in our case is the Base64 encoding of test:testpwd defined in the inMemoryAuthentication

I hope this is usefull

Angelo

WEB SECURITY USER DATAIL SERVICE

In order to configure Spring security to retrieve user details from DB you must do the following:

create a org.springframework.security.core.userdetails.UserDetailsService implementation like this:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private BasicService svc;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        BasicUser result = svc.findByUsername(username);
        if( result == null )
        {
            throw new UsernameNotFoundException("No user found with username "+username);
        }
        return result;
    }

}

Inject it to the spring security configuration and use it like this:

public class WebSecConfig extends WebSecurityConfigurerAdapter {
    @Autowired private RestAuthEntryPoint authenticationEntryPoint;
    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//      auth
//      .inMemoryAuthentication()
//      .withUser("test")
//      .password(passwordEncoder().encode("testpwd"))
//      .authorities("ROLE_USER");
        auth.userDetailsService(userDetailsService);
        auth.authenticationProvider(authenticationProvider());
    }
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .authorizeRequests()
        .antMatchers("/securityNone")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .httpBasic()
        .authenticationEntryPoint(authenticationEntryPoint);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

I pushed the code on the github link I provided. There you can find a full working example based on:

  • spring 5
  • spring security 5
  • hibernate
  • h2 DB

Feel free to adapt it to your own scenario

Angelo Immediata
  • 6,635
  • 4
  • 33
  • 65
  • One more question. Correct me if I'm wrong: I see that user credentials are set during app startup. How I can add methods for DB query for every request? – Peter Penzov Sep 04 '18 at 10:49
  • I told you I’m using inmemory authentication. You should change it by creating your own detail service – Angelo Immediata Sep 04 '18 at 10:50
  • It turns out that for some links I don't need to use authentication. Please check the updated post. – Peter Penzov Sep 05 '18 at 09:43
  • Well in this case you must properly configure spring security. Give a look to my configuration. I configure it in order that all requests with path `securityNone` are not protected and so require no authentication. All other requests must have authentication. You should change this configuration according to your own scenario – Angelo Immediata Sep 05 '18 at 09:49
  • One last request. Can you modify the code to use service for database authentication, please? – Peter Penzov Sep 05 '18 at 10:16
  • OK... but please give me 1 hour.. I have to finish some activity here :) – Angelo Immediata Sep 05 '18 at 10:16
  • I edited the answer. See the **WEB SECURITY USER DATAIL SERVICE** section – Angelo Immediata Sep 05 '18 at 13:18
  • Thanks I will test it later today. – Peter Penzov Sep 05 '18 at 13:44
  • I tried to implement the code but I found a little problem here auth.userDetailsService(userDetailsService); I don't have only username. I have username and password. I need to authenticate both which hare send as `Authorization: Basic OWEwYmJjNDU5ZGZmYTcwY2FhY2JkYmU5YzIzOWMyZmUxNmY4ZjU2MjozRElwZnBMbGZhcW9NVnNSck0yZWFqY3pLUVBSeWwwRFZabDE1UTR1` into http header. Is there a way to implement other way database authentication? – Peter Penzov Sep 06 '18 at 21:03
  • This is done automatically by spring security by using the password encoder (this is the reason we use it) try to use a wrong password ... it should return error – Angelo Immediata Sep 06 '18 at 21:07
  • And one more issue. I store username and password in entirely different Entity model. Probably I have to return in my query only username and password? Not entire object? – Peter Penzov Sep 06 '18 at 21:34
  • This is not an issue. Of course I created a very simple sample. Now you have to adapt it to your own scenario. This means: configure spring security and urls properly. Change the sample query to the real query and so on. – Angelo Immediata Sep 07 '18 at 04:05
  • I will try to implement it. Thanks! – Peter Penzov Sep 07 '18 at 06:36
  • One more question: IN which file I have to configure links which don't require authentication? – Peter Penzov Sep 07 '18 at 06:37
  • In the class `WebSecConfig` in method `protected void configure(HttpSecurity http)`. Here I configured a non protected access to the URL `/securityNone` because I used `permittAll()`; all others URL are protected because I used `.anyRequest().authenticated()`. You should change `/securityNone` with oyur unprotected URL (e.g. `/public/*`) – Angelo Immediata Sep 07 '18 at 06:41
  • I need to ask a question. How I can configure Spring Security NOT to set role for the user and use password in authentication. Ref https://stackoverflow.com/questions/52225299/configure-spring-to-use-database-for-authentication – Peter Penzov Sep 07 '18 at 20:37
  • Well its not good thing to not use roles. I strongly suggest to you to re-design your scenarios in using roles. In any case Spring security UserDetails needs at least a role. So you can create a "fake" role never used in your application. Regarding to the password issue... as I told you it's not an issue. After the loading of userdetail spring Security checks the password by using the PasswordEncoder. What I suggest to you is to read deeply the PasswordEncoder section (https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/htmlsingle/#core-services-password-encoding) – Angelo Immediata Sep 09 '18 at 09:11
1

You can use a default spring-security configuration described on various websites, like baeldung.com or mkyong.com. The trick in your sample seems to be the call to get the Merchant. Depending on the complexity of the authenticationService and the Merchant object, you can either use the following code, or implement a facade to get similar behaviour.

@Autowired
public void authenticationManager(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(new AuthenticationProvider() {
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
            if(merchant == null) {
                throw new AuthenticationException("No Merchant found.");
            }
            return new UsernamePasswordAuthenticationToken(name, password, merchant.getAuthorities());
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
        }
    });
}

Setting the attribute on the request, if necessary could be done by a separate filter which takes the Principal from the SecurityContext and puts it on the request as an attribute.

Roald Bankras
  • 564
  • 4
  • 14