Securing your APIs when building Web Applications

What are we developing today

Let’s assume for a moment that we have come up with a brilliant idea that will make the world so much better than it is today. All we need to do next is to build a web application and give in the hands of billions of people to use.

Among many things to build, the most common ones are developing your APIs and securing them. Also, you need a way to allow only authenticated users use your APIs.

Why you do not want to create user/passwords in your application?

The users are not comfortable in creating and remembering the new passwords every time they want to use a web application. Also, there is usually a trust issue where users believe they do not trust any new application coming in the market. What if their passwords are stored in plain-text and someone will eventually hack them (an example, and recent incident)? You do not want to be such a service, right?

Users, these days are rather comfortable delegating their authentication via services like Google/Facebook/Twitter since they use those services almost everyday and trust that Google will keep their passwords secure.

Also, if they do not like any service, they can revoke the access to the application via Google/Facebook/Twitter console, but once they have given the password to a new application, there is nothing much they can do

How do you want to authenticate users to your applications?

Given the benefits not maintaining user/passwords in our application, let us roll out our first version where users can login with google. A typical workflow may look like following

Users sign in to your application via Google

Step 01: User clicks on Login with Google

Step 02: Google login screen pops up requesting the user to login

After this step, Google presents a screen for the user to allow/deny the access to the application. The screen looks similar to following

Google asks user to allow/deny the access to application

Step 03: The web application calls your application APIs and the gets the results from your server.

This workflow is known as OAuth for Client-side web applications.

But wait a minute! If a browser is calling your APIs, how do you trust that the an authenticated Google user is making this call? It could be anyone, right?

Securing your application APIs

As per the diagram above, so far, the trust has been developed between the user (using the web application) and Google servers, but there is no way our application server knows anything about this trust relationship and there is no way for it to validate either.

One possible option

One of the ways to solve this problem is to always secure your APIs using a mechanism that will be followed by all the consumers of your APIs. The well-known approach in this area is Token Based Authentication. Let’s understand this by extending the above digram further

Securing your APIs with trust

Let’s continue from Step 03

Step 03: Client (web application) sends a request to your API to login. They provide ID-TOKEN acquired during google authentication for this communication step.

Step 04: Application validates the ID-TOKEN with Google. This is documented here. With this verification, your app can trust that the user is actually what they are pretending to be. This develops the trust between the client and your application server.

Step 05: You would not want to perform this verification on every API call since there are performance issues to this approach and you might be rate limited by Google. Rather, build your own application token which can be verified securely and efficiently (we will see this code later in this post). Let us call this APP-TOKEN. Your APP-TOKEN should have expiry date so that it can not be misused. At this time, you may also want to add this user to your database if not added previously.

Step 06: Client asks for data from your server by presenting the token your app issued. Since only you can verify that token, you can do this without going to Google. You send the data to the client along with a new refreshed APP-TOKEN.

Pre-requisites

You must have a Google Client Id to run this project. You can create one here

What technologies are we using

With that we should see some code to see it in action. We are going to use Spring-boot, Java 8, JWT and Google’s Java client library.

The codebase for this and tests are available at https://github.com/hhimanshu/google-oauth2-jwt-secure-api

We will discuss they classes that made this workflow possible.

@Component
public class GoogleTokenVerifier {

private static final HttpTransport transport = new NetHttpTransport();
private static final JsonFactory jsonFactory = new JacksonFactory();
private static final String CLIENT_ID = "YOUR-GOOGLE-CLIENT-ID";


public Payload verify(String idTokenString)
throws GeneralSecurityException, IOException, InvalidTokenException {
return GoogleTokenVerifier.verifyToken(idTokenString);
}

private static Payload verifyToken(String idTokenString)
throws GeneralSecurityException, IOException, InvalidTokenException {
final GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.
Builder(transport, jsonFactory)
.setIssuers(Arrays.asList("https://accounts.google.com", "accounts.google.com"))
.setAudience(Collections.singletonList(CLIENT_ID))
.build();


System.out.println("validating:" + idTokenString);

GoogleIdToken idToken = null;
try {
idToken = verifier.verify(idTokenString);
} catch (IllegalArgumentException e){
// means token was not valid and idToken
// will be null
}

if (idToken == null) {
throw new InvalidTokenException("idToken is invalid");
}

return idToken.getPayload();
}
}

This class takes the ID-TOKEN coming from client and validates with Google using Google’s Java Client library.

@Component
public class AppTokenProvider {

private static final long EXPIRATION_TIME_SECONDS = 864_000_000; // 10 days
private static final String SECRET = "ThisIsASecret";
private static final String TOKEN_PREFIX = "Bearer";
private static final String HEADER_STRING = "Authorization";

public static void addAuthentication(HttpServletResponse res, String userId) {
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + getNewPerishableToken(userId));
}

private static String getNewPerishableToken(String userId) {
return Jwts.builder()
.setSubject(userId)
.setExpiration(new Date(System.nanoTime() + EXPIRATION_TIME_SECONDS))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}

public String getNewPerishableTokenFor(String userId) {
return getNewPerishableToken(userId);
}

public static Optional<String> getUserFromToken(HttpServletRequest request) {
final String token = request.getHeader(HEADER_STRING);

if (token != null) {
try {
Claims body = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody();
final Instant expiration = body.getExpiration().toInstant();

if (expiration.isBefore(Instant.now())) {
return Optional.empty();
}
return Optional.of(body.getSubject());

} catch (SignatureException e) {
// invalid signature
}
}
return Optional.empty();
}
}

This class creates token for your application and also have a method to validate the token coming from the client

We are using JWT to create the token for our application. JWT is RFC 7519 standard for representing and sharing claims securely between communicating parties.

The JWT token is usually represented as following

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Where each section is separated by . and is known as header , payload and signature respectively

Header looks similar to

{ “alg”: “HS256”, “typ”: “JWT” }

which means we are using SHA-256 hashing algorithm that we are using and token is of type JWT.

Payload is divided into 3 type of claims: reserved , public and private

reserved includes claims including the following

iss (issuer): that says who issued this token (or owner of the token)

sub(subject): subject of the token or to whom this token is issued

exp(expiration): when does this token expire

An example of payload may look like

{ “sub”: “1234567890”, “name”: “John Doe”, “iss”: “Google” }

This is issued by Google to “John Doe” with id 1234567890.

public claims are the claims to be used by the consumers of JWT tokens.

private claims are the custom claims, that communicating parties agree to share.

Signature is created using the following formula

HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)

This indicates that only owner can generate and verify the token and rest assured that no one else could tamper it in transit (since only owner knows the secret).

@Component
public class LoginFilter implements Filter {

private GoogleTokenVerifier googleTokenVerifier;

@Autowired
public LoginFilter(GoogleTokenVerifier googleTokenVerifier) {
this.googleTokenVerifier = googleTokenVerifier;
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {

String idToken = ((HttpServletRequest) servletRequest).getHeader("X-ID-TOKEN");
HttpServletResponse response = (HttpServletResponse) servletResponse;

if (idToken != null) {
final Payload payload;
try {
payload = googleTokenVerifier.verify(idToken);
if (payload != null) {
String username = payload.getSubject();
AppTokenProvider.addAuthentication(response, username);
filterChain.doFilter(servletRequest, response);
return;
}
} catch (GeneralSecurityException | InvalidTokenException e) {
// This is not a valid token, we will send HTTP 401 back
}
}
((HttpServletResponse) servletResponse).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}

@Override
public void destroy() {
}
}

This class intercepts the incoming call to /login, if the X-ID-TOKEN is present it validates further, if not, it does not forwards the request to API endpoint and return HTTP 401 Unauthorized.

@Component
public class RestFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

Optional<String> userFromToken = getUserFromToken(request);
if (!userFromToken.isPresent()) {
response.sendError(HttpStatus.UNAUTHORIZED.value());
return;
}

addAuthentication(response, userFromToken.get());
filterChain.doFilter(request, servletResponse);
}

@Override
public void destroy() {
}
}

This class intercepts every call coming to /rest based URL pattern. It checks for Authorization header and validates the token if present. It also then adds a new refreshed Authorization header that client may want to use for further communication. If the token is not present, or is invalid, the call does not hits the endpoints and return HTTP 401 UnAuthorized.

But we just created the filters, how do these filters know which URL patterns to intercept? Good question. We have just created the filters, the next step is to register them so that they are active and intercept the incoming calls. We do that by following

@Configuration
public class Filters {

private final LoginFilter loginFilter;
private final RestFilter restFilter;

@Autowired
public Filters(LoginFilter loginFilter, RestFilter restFilter) {
this.loginFilter = loginFilter;
this.restFilter = restFilter;
}

@Bean
public FilterRegistrationBean loginRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(loginFilter);
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/login/*"));
return filterRegistrationBean;
}

@Bean
public FilterRegistrationBean restRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(restFilter);
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/rest/*"));
return filterRegistrationBean;
}
}

As we see, we registered 2 filters for 2 different URL patterns here.

All our API endpoints are available are

package com.hhimanshu.secure.api

There is one important piece, which is enabling the web security for Spring-Boot based application. This is done by extending WebSecurityConfigurerAdapter and looks like

@EnableWebSecurity //(debug = true) // when you want to see what filters are applied
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/css/**", "/js/**", "/images/**", "/static/**", "/**/favicon.ico").permitAll()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers("/rest/*").permitAll()
.antMatchers("/").permitAll()
.anyRequest().authenticated();
}
}

What we are doing here is following

  • Overriding the default HTTP based authentication which is Basic Authentication.
  • We disable csrf protection on the server since we are using token based authentication. Since we are not using cookies, we do not have to worry about CSRF based attacks. You may want to read about them here
  • We allow access to static assets. This is if you also serve front-end from the same server as backend, those calls are not required to be authenticated
  • We allow HTTP POST to our server at /login request
  • We allow all requests on URL patterns /rest/*, where our protected resources live and we also allow top level root access /, to serve index.html
  • We configure that any other type of request to our server otherwise should be authenticated.
  • For /login and /rest/* based endpoints, our filters LoginFilter and RestFilter intercept and validate the request before forwarding it to the endpoints to serve.

The complete workflow has been tested in the following test. It mocks the calls to Google and executes both the cases where token is valid and invalid. You can see how our filters work

The entire code is hosted on Github and contains all the necessary documentation to fork and run. It is also connected to Travis CI to demonstrate the build and test process.