Friday, August 19, 2016

Manage your API Keys with Java, Jersey, and Stormpath

If you are a Java developer, then you are undoubtedly familiar with frameworks such as Spring, Play!, and Struts. While all three provide everything a web developer wants, I decided to write a RESTful web application using the Jersey framework. This sample app uses Java + Jersey on the back-end and Angular JS on the front-end.
Jersey annotation service makes it easy to do routing, injection, and other functions important to a RESTful web application. My goal was to demonstrate the use of the Stormpath Java SDK for user management and the protection of a REST endpoint using API Keys and Oauth Tokens, all while relying on Jersey.
You can check out the Stormpath Jersey sample app in github, and follow along here for the implementation details and concepts I found most important while building this application. I will explain Stormpath SDK calls, Jersey annotations, and the general flow of the application, so it the codebase is easy to decipher.
Let’s code!

Login

Stormpath provides username/password authentication in three lines of Java SDK method calls. That makes it very simple to launch a basic login form, securely.

As soon as a user enters their credentials and clicks the “Sign In” button, an AJAX request is made to the /login endpoint. Let’s take a look at the server side login code:
       
@Path("/login")

public class Login {



@Context

private HttpServletResponse servletResponse;



@POST

@Consumes(MediaType.APPLICATION_JSON)

public void getDashboard(UserCredentials userCredentials) 

  throws Exception {



  Application application = StormpathUtils.myClient.getResource(

    StormpathUtils.applicationHref, Application.class);



  AuthenticationRequest request = new UsernamePasswordRequest(

    userCredentials.getUsername(), userCredentials.getPassword());



  Account authenticated;



  //Try to authenticate the account

  try {

   authenticated = application.authenticateAccount(request).getAccount();



  } catch (ResourceException e) {

    System.out.println("Failed to authenticate user");

    servletResponse.sendError(401);

    return;

  }



  Cookie myCookie = new Cookie("accountHref", authenticated.getHref());

  myCookie.setMaxAge(60 * 60);

  myCookie.setPath("/");

  myCookie.setHttpOnly(true);

  servletResponse.addCookie(myCookie);

  }

}
Here we see three examples of Jersey’s annotation feature. The @Path annotation acts as our router. The @Context annotation injects the HTTP Request object into our class. Finally, the @POST specifies the CRUD operation.
User authentication is done by first creating an Application object, creating an AuthenticationRequest object, and finally calling application.authenticationAccount(request) to ask Stormpath to authenticate this account.

Create Account

Creating an account is just as simple as logging in to the service:

       
@Path("/makeStormpathAccount")
public class StormpathAccount {
@POST
public void createAccount(UserAccount userAccount) throws Exception {
  Application application = StormpathUtils.myClient.getResource(
    StormpathUtils.applicationHref, Application.class);
  Account account = StormpathUtils.myClient.instantiate(Account.class);
  //Set account info and create the account
  account.setGivenName(userAccount.getFirstName());
  account.setSurname(userAccount.getLastName());
  account.setUsername(userAccount.getUserName());
  account.setEmail(userAccount.getEmail());
  account.setPassword(userAccount.getPassword());
  application.createAccount(account);
  }
}
All we had to do here, was create an Application and an Account, set the Account attributes, and call createAccount.

Generating an API Key ID/Secret

Once a user logs in, they will be given API Key credentials. In this application, generation of the Keys is a simple AJAX call to /getApiKey:
       
@Path("/getApiKey")
public class Keys {
@Context
private HttpServletRequest servletRequest;
@Context
private HttpServletResponse servletResponse;
@GET
@Produces(MediaType.APPLICATION_JSON)
public Map getApiKey(@CookieParam("accountHref") String accountHref) throws Exception {
  Account account = StormpathUtils.myClient.getResource(accountHref,
    Account.class);
  ApiKeyList apiKeyList = account.getApiKeys();
  boolean hasApiKey = false;
  String apiKeyId = "";
  String apiSecret = "";
  //If account already has an API Key
  for(Iterator<ApiKey> iter = apiKeyList.iterator(); iter.hasNext();) {
     hasApiKey = true;
     ApiKey element = iter.next();
     apiKeyId = element.getId();
     apiSecret = element.getSecret();
  }
  //If account doesn't have an API Key, generate one
  if(hasApiKey == false) {
     ApiKey newApiKey = account.createApiKey();
     apiKeyId = newApiKey.getId();
     apiSecret = newApiKey.getSecret();
  }
  //Get the username of the account
  String username = account.getUsername();
  //Make a JSON object with the key and secret to send back to the client
  Map<String, String> response = new HashMap<>();
  response.put("api_key", apiKeyId);
  response.put("api_secret", apiSecret);
  response.put("username", username);
  return response;
}
}

We use Jersey’s @CookieParam annotation to grab the account Href from the Cookie that was created at login. We create an account, and an ApiKeyList object. We then check if this account already has an API Key. If so, our job is to simply request it from Stormpath; if not, we tell Stormpath to make a new one for this account and return this back to the client. By Base64 encoding the API Key:Secret pair, a developer can now target our endpoint using Basic authentication:

Using a Jersey Filter

A cool feature of the Jersey framework is its Request filter. By implementing ContainerRequestFilter we can intercept an HTTP request even before it gets to our endpoint. To demonstrate, I added an additional level of security around API Key generation. Before a user is allowed to target the /getApiKey endpoint they must pass through the Jersey request filter, which will check if the client is actually logged in (a.k.a has a valid session in the form of a cookie).
       
@Provider
public class JerseyFilter implements ContainerRequestFilter {
@Context
private HttpServletResponse servletResponse;
@Override
public void filter(ContainerRequestContext requestContext) throws
  IOException {
  URI myURI =  requestContext.getUriInfo().getAbsolutePath();
  String myPath = myURI.getPath();
  if(myPath.equals("/rest/getApiKey")) {
    Iterator it = requestContext.getCookies().entrySet().iterator();
    String accountHref = "";
    while(it.hasNext()) {
      Map.Entry pairs = (Map.Entry)it.next();
      if(pairs.getKey().equals("accountHref")) {
       String hrefCookie = pairs.getValue().toString();
       accountHref =
        hrefCookie.substring(hrefCookie.indexOf("https://"));
      }
        }
    if(!accountHref.equals("")) {
      //Cookie exists, continue.
      return;
    }
    else {
      System.out.println("Not logged in");
      servletResponse.sendError(403);
    }
  }
}
}

If a client is trying to get an API Key without being logged in, they will get a 403 before even reaching the actual endpoint.

Exchanging your API Keys for an Oauth Token

Want even more security? How about trading your API Key for an Oauth Token? Using Oauth also brings the functionality of scope, which we can use to allow users to get weather from specified cities.

Let’s take a look at the code:
       
@Path("/oauthToken")
public class OauthToken {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public String getToken(@Context HttpHeaders httpHeaders,
                       @Context HttpServletRequest myRequest,
                       @Context final HttpServletResponse servletResponse,
                       @FormParam("grant_type") String grantType,
                       @FormParam("scope") String scope) throws Exception {
/*Jersey's request.getParameter() always returns null, so we have to
    reconstruct the entire request ourselves in order to keep data
See: https://java.net/jira/browse/JERSEY-766
*/
  Map<String, String[]> headers = new HashMap<String, String[]>();
  for(String httpHeaderName : httpHeaders.getRequestHeaders().keySet()) {
    //newBuilder.header(String, String[]);  
    List<String> values = httpHeaders.getRequestHeader(httpHeaderName);
    String[] valueArray = new String[values.size()];
    httpHeaders.getRequestHeader(httpHeaderName).toArray(valueArray);
    headers.put(httpHeaderName, valueArray);
  }
  Map<String, String[]> body = new HashMap<String, String[]>();
  String[] grantTypeArray = {grantType};
  String[] scopeArray = {scope};
  body.put("grant_type", grantTypeArray );
  body.put("scope", scopeArray);
  HttpRequest request = HttpRequests.method(HttpMethod.POST).headers(
    headers).parameters(body).build();
  Application application = StormpathUtils.myClient.getResource(  
    StormpathUtils.applicationHref, Application.class);
  //Build a scope factory
  ScopeFactory scopeFactory = new ScopeFactory(){
    public Set createScope(AuthenticationResult result,
      Set requestedScopes) {
      //Initialize an empty set, and get the account
      HashSet returnedScopes = new HashSet();
      Account account = result.getAccount();
        /***
        In this simple web application, the scopes that were sent in the
        body of the request are exactly the ones we want to return. If
        however we were building something more complex, and only wanted
        to allow a scope to be added if it was verified on the server
        side, then we would do something as shown in this for loop. The
        'allowScopeForAccount()' method would contain the logic which
        would check if the scope is truly allowed for the given account.
        for(String scope: requestedScopes){
                if(allowScopeForAccount(account, scope)){
                    returnedScopes.add(scope);
                }
            }
        ***/
      return requestedScopes;
    }
  };
  AccessTokenResult oauthResult = application.authenticateOauthRequest(
    request).using(scopeFactory).execute();
  TokenResponse tokenResponse = oauthResult.getTokenResponse();
  String json = tokenResponse.toJson();
  return json;
}
}

Notice the 10 lines of code right after the getToken() declaration. This is a workaround for Jersey’s lack of providing us with a complete request object. Calling request.getParamter() or request.getParameterMap() will always return null, and since creating an AccessTokenResult object requires the Request object with the body still intact, we must recreate the entire request ourselves.

Finally: Securing your REST endpoint

Ahh, the moment we’ve all been waiting for. Now that we have given our users the ability to target this weather endpoint using Basic and Oauth authentication, it is up to us to figure out which protocol they choose to use.
       
@Path("/api/weather/{city}")
public class WeatherApi {
@Context
private HttpServletRequest servletRequest;
@Context
private HttpServletResponse servletResponse;
private String weatherResult;
@GET
@Produces(MediaType.APPLICATION_JSON)
public String getWeatherApi(@PathParam("city") final String myCity)
  throws Exception {
  Application application = StormpathUtils.myClient.getResource(
    StormpathUtils.applicationHref, Application.class);
  System.out.println(servletRequest.getHeader("Authorization"));
  //Make sure this use is allowed to target is endpoint
  try {
    ApiAuthenticationResult authenticationResult =
      application.authenticateApiRequest(servletRequest);
    authenticationResult.accept(new AuthenticationResultVisitorAdapter() {
      public void visit(ApiAuthenticationResult result) {
        System.out.println("Basic request");
        URL weatherURL = getURL(myCity);
        //Parse weather data into our POJO
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        City city = null;
        try {
           InputStream in = weatherURL.openStream();
           city = mapper.readValue(in, City.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        weatherResult = city.toString() + " °F";
      }
      public void visit(OauthAuthenticationResult result) {
        //Check scopes
        if(result.getScope().contains("London") && myCity.equals("London")){
          weatherResult = getWeather(myCity) + " °F";;
        }
        else if(result.getScope().contains("Berlin") && myCity.equals("Berlin")){
          weatherResult = getWeather(myCity) + " °F";;
        }
        else if(result.getScope().contains("SanMateo") && myCity.equals("San Mateo")){
          weatherResult = getWeather(myCity) + " °F";;
        }
        else if(result.getScope().contains("SanFrancisco") && myCity.equals("San Francisco")){      
          weatherResult = getWeather(myCity) + " °F";;
        }
        else {
          try {
            servletResponse.sendError(403);
          } catch (IOException e) {
            /* To change body of catch statement use File | Settings | File Templates.*/
            e.printStackTrace();  
          }
        }
      }
    });
  return weatherResult;
} catch (ResourceException e) {
  System.out.println(e.getMessage());
  servletResponse.sendError(403);
  return "Cannot authenticate user.";
}
}

To do this we use a visitor. We create a visitor for each type of authentication protocol that we expect our clients to use (in our case Basic and Oauth). Based on the type of the ApiAuthenticationResult object, the appropriate visitor will be targeted. Notice how inside the OauthAuthenticationResult visitor, we check the scope of the Oauth token that we received, and appropriately give/forbid access to the requested cities.
When we generated our Oauth token in the sceenshot above, we gave access to view weather in London, Berlin, and San Francisco. Thus we can view London’s weather using Oauth:

However, since San Mateo was not included in the scope of the Oauth token, we cannot see its weather:

Conclusion

Jersey is yet another Java framework that seamlessly integrates with the Stormpath SDK to offer user management, API Key management, Oauth, and more. If you’d like to see more code and even run this application yourself please visit: https://github.com/rkazarin/sample-jersey-webapp

No comments:

Post a Comment