6 ways: how to add security for Rest service in Java

In this article I will try to describe several ways, or rather 6, how to add security for rest service in Java.

Our team was tasked with finding all possible ways to add security to the rest service. To analyze the pros and cons and choose the most suitable for our project. When I started looking for such an article in Google I didn’t find anything suitable, but there were only fragments and I had to collect this information bit by bit. So I think this article will also be useful to other Java developers writing back-end. I will not argue that any of these methods is better or worse, it all depends on the task and the specific project. Therefore, which of the six ways is most suitable for your project is up to you. I will try to describe the principle of each approach and give a small example using Java and Spring Security.

Method One : Basic Authentication


Basic Authentication - the user or rest client indicates his username and password to gain access to the rest service. Login and password are transmitted over the network as plaintext encoded by simple Base64 and can be easily decoded by any user. When using this method, the https protocol must be used for data transfer.

The configuration is very simple, it will look like security.xml for our Spring Security


This is our rest controller:

@RequestMapping("/rest/api")
@RestController
public class RestController {
    @RequestMapping
    public Object getInfo() {
        return //some response MyClass;
    }
} 

And finally, a rest client based on the RestTemplate sprin. In the header, add the word Basic followed by a username and password without spaces, separated by a colon and encoded by Base64.

        RestTemplate restTemplate = new RestTemplate();
        String url = "http://localhost:8080/rest/api";
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Basic QWxhZGRpbupvcRVuIHNlc2FtZQ=="); //here is some login and pass like this login:pass
        HttpEntity request = new HttpEntity(headers);
        MyClass myclass = restTemplate.exchange(url, HttpMethod.GET, request, MyClass.class).getBody();

Method Two : Digest authentication


Digest authentication - This is almost the same as the first method, only the login and password are transmitted in encrypted form, and not as plain text. Login and password are encrypted with MD5 algorithm and it is quite difficult to decrypt it. With this approach, you can use an insecure http connection and are not afraid that the password will be intercepted by attackers. The implementation of the server side remains the same. It is necessary to change the client a little so that it can send the password in encrypted form. Here the http Apache client will come to our aid.
In order not to engage in copy paste, I will provide a link to a project on Github with the implementation of such a client .

Method Three : Token Authentication


The essence of this method is that the user using his credentials logs into the application and receives a token to access the rest service. Access to the service that issues tokens must necessarily be done through an https connection, access to the rest service can be done through regular http. The token must contain a login, password, it can also contain expiration time and user roles, as well as any information necessary for your application. After the token is ready and, for example, all its parameters are separated by a colon or other symbol convenient for you or serialized as a json or xml object, it must be encrypted before being sent to the user. Please note that only a rest service should know how to decrypt this token. After the token arrives at the rest service, it decrypts it and receives all the necessary data for authentication and if it is necessary to authorize the rest client. Implementation will be radically different from the previous two.

Our security.xml will now look like this:


The RestAuthenticationEntryPoint bean will look something like this:

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

Filter CustomTokenAuthenticationFilter, which will check the validity of the token, rights, etc. and ultimately deciding whether this client is allowed to work with our rest service or not, will look something like this, but you can implement it differently.

public class CustomTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private AuthenticationManager authenticationManager;
    @Autowired
    private CryptService cryptService; //service which can decrypt token
    public CustomTokenAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(defaultFilterProcessesUrl);
        this.authenticationManager = authenticationManager;
        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(new NoOpAuthenticationManager());
        setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler());
    }
    public final String HEADER_SECURITY_TOKEN = "My-Rest-Token";
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        String token = request.getHeader(HEADER_SECURITY_TOKEN);
        Authentication userAuthenticationToken = parseToken(token);
        if (userAuthenticationToken == null) {
            throw new AuthenticationServiceException("here we throw some exception or text");
        }
        return userAuthenticationToken;
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);
        chain.doFilter(request, response);
    }
    // This method makes some validation depend on your application logic
    private Authentication parseToken(String tokenString) {
        try {
            String encryptedToken = cryptService.decrypt(tokenString);
            Token token = new ObjectMapper().readValue(encryptedToken, Token.class);
                return authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(token.getUsername(), token.getPassword()));
        } catch (Exception e) {
            return null;
        }
        return null;
    }
}

What do we have in the end. The user logs into the application receives an encrypted token, which can use the spring RestTemplate or another rest client adding it to the header, for example, our custom header My-Rest-Token. On the server side, the filter receives the value from this header, decrypts the token, parses it or parses it into components and decides whether or not to give access to the client.

Method Four : Digital Signature (public / private key pair)


The idea behind this approach is to use a public key cryptosystem . The bottom line is that anyone can turn to the rest service and get a random set of characters, or rather an encrypted response from the server and only the owner of the private key can decrypt it.
And so in order.
  1. When a new user is registered on the server, a pair of keys is generated for this user - public and private
  2. Private is sent to the user and only he can decrypt the message (the key must be sent through a secure channel so that no one can intercept it)
  3. At each rest request, the client sends its username so that the service can encrypt the message with the desired public key
  4. Service encrypts and sends a message
  5. The client accepts it and decrypts with its key

To implement this approach, two filters must be written: one on the server side, the other on the client side. The filter on the server side of the rest server will encrypt rispon using the key of the client who made the request. The rest client filter will decrypt the rispon from the rest service using its private key.

This approach can be made even safer if you generate a key pair on the client side using javascript libraries such as forge. This approach allows you not to send the private key over the network at all, but to immediately generate it on the client side, which significantly reduces the risk of compromising this key. The public key is sent to the server for later use in message encryption. The sending channel may be insecure, since there is nothing to worry about if the public key is intercepted (for details, see the link above the public key cryptosystem).

Method Five : Certificate Authentication


You can configure your server in such a way that if the client does not provide the required certificate with the request, it will not receive a response from the server, or rather it will receive a response that the certificate is missing or not suitable. Read more about certificates here . Certificates come in two types:
  • Trusted - those that everyone can check and they are registered in a single certification center
  • Self signed - those that you generate yourself and you need to add them to your rest service as an exception so that it knows about their existence and that they can be trusted

Below are a few steps on how to create a Self signed certificate using the keytool utility.

generate client and server keys
keytool -genkey -keystore keystore_client -alias clientKey
keytool -genkey -keystore keystore_server -alias serverKey


generate client and server certificates
keytool -export -alias clientKey -rfc -keystore keystore_client> client.cert
keytool -export -alias server rfc -keystore keystore_server> server.cert


import certificates to corresponding truststores
keytool -import -alias clientCert -file client.cert -keystore truststore_server
keytool -import -alias serverCert -file server.cert -keystore truststore_client


Now the received certificates must be added to our server configuration. In this case, Tomcat is used.

The following is a rest client using the Http Apache Client, which is able to provide a certificate to the rest service and will carry out all the necessary “handshakes” to receive a response from the server

public class CertificateAuthenticationServiceImpl implements CertificateAuthenticationService {
    private static final String keyStorePass = "changeit";
    private static final String trustedStorePass = "changeit";
    private static final File keyStore = new File(new CertificateAuthenticationServiceImpl().getClass().getResource("/authCertificate/keystore_client").getPath());
    private static final File trustedStore = new File(new CertificateAuthenticationServiceImpl().getClass().getResource("/authCertificate/truststore_client").getPath());
    private static final String certificateType = "jks";
    public String httpGet(URL url) {
        String resp = null;
        try {
            final HttpParams httpParams = new BasicHttpParams();
            final KeyStore keystore = KeyStore.getInstance(certificateType);
            keystore.load(new FileInputStream(keyStore), keyStorePass.toCharArray());
            final KeyStore truststore = KeyStore.getInstance(certificateType);
            truststore.load(new FileInputStream(trustedStore), trustedStorePass.toCharArray());
            final SchemeRegistry schemeRegistry = new SchemeRegistry();
            schemeRegistry.register(new Scheme(url.toURI().getScheme(), new SSLSocketFactory(keystore, keyStorePass, truststore), url.getPort()));
            final DefaultHttpClient httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, schemeRegistry), httpParams);
            try {
                HttpGet httpget = new HttpGet(url.toString());
                CloseableHttpResponse response = httpClient.execute(httpget);
                try {
                    HttpEntity entity = response.getEntity();
                    if (entity != null) {
                        resp = EntityUtils.toString(entity);
                    }
                    EntityUtils.consume(entity);
                } finally {
                    response.close();
                }
            } finally {
                httpClient.close();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return resp;
    }
}

Method Six : OAuth2 authorization


Well, for starters, I left the most difficult to understand and implement method. But it is very flexible and well suited for large portals. Again, I will not do copy-paste to read what OAuth is and how it works, we go here .
Spring security provides us with the OAuthTemplate class, which makes our life much easier.
I got all the ideas for implementing my OAuth implementation from this wonderful article; there is even a working draft that can be downloaded.

Conclusion


Well, I hope I managed to clarify the big picture a bit and help you in implementing your own projects. Of course, these are not all ways to protect your rest service, but there are solutions for every taste. Implementation examples are not general solutions, but are only given for a complete understanding of the picture. You can write your own implementation depending on the needs of the project.

Before choosing security for your project, clearly decide what you need, weigh the pros and cons. Do not complicate the system if this is not required by the project.

I hope your applications will be safe and reliable.

Also popular now: