Sunday, April 8, 2018

Handling EVE ESI Authentication with Spring Boot

Continuing on from my prior post on reading from the EVE ESI API with Java, today I provide some details on handing the EVE ESI authentication flow with Spring Boot.

With ESI, they expect your application to be a web application, whether it is one or not.  If it's not, things get more complicated.  The problem stems from their new flow where you are supposed to redirect to them for EVE sign on and then they redirect back to you with a code you use to obtain access to restricted data.  This is problematic if your application isn't a web application.

If you are using Java, this problem happens to be an ideal use for Spring Boot.  With Spring Boot, configuration is mostly a breeze and an application server like Tomcat can be easily embedded in your Spring Boot application to handle the redirect and callback.

First, a quick overview of the steps you need to take, then below I provide an example Controller class for a Spring Boot application that can handle all of this.

  1. Determine your IP address, pick a port, choose a callback request mapping, and build the URL for your application from all that.  (example: http://[your IP]:[port]/[request mapping]).  It is recommended you use https, but doing so will require additional setup I'm not going into here.
  2. Register your application at the EVE Developers Site, using the URL from the previous step as your application callback URL.
  3. Access your router and forward TCP for your port to the machine that will be running your application.
  4. Create your Spring Boot application.  Maven dependencies provided below the Controller example should you find it helpful.
  5. Run your application.  Following the controller flow intended for the example, you would have an index page with a link to your /launchSignOn page.  Clicking that should redirect to EVE where you sign in.  Following that, it should redirect back to your application, where your application then takes the code sent back, makes a post request to get an access token (which includes a refresh token you will want), and makes a request to get character info, and then do whatever.  If you are just trying to get a refresh token and character ID for a different application, it can just display the fresh token and character ID on the screen for your to copy down.

Example Spring Boot Controller 


@Controller
public class SampleController {

 // these variables could come from a properties file or elsewhere, shown here as constants 
 // just for the sake of this example
 private static final String SCOPE = "esi-markets.read_character_orders.v1";  // example
 private static final String MY_CLIENT_URL = "?/eveCallback";  // callback URL for your application
 private static final String MY_CLIENT_ID = "?";   // client ID for your registered EVE application
 private static final String MY_SECRET_KEY = "?";  // secret key for your registered EVE application
 private static final String STATE = "?";            // your application state, if any
 private static final String MY_REFRESH_TOKEN = "?";  // saved after first sign in for subsequent use
 
 /**
  * A start page, should you want one.  It could have links to /launchSignOn and /testRefreshToken
  * 
  * @return view page reference
  */
 @RequestMapping(value="/", method=RequestMethod.GET)
 public String startPage() {
  return "index";
 }
 
 /**
  * Begin authentication process.  This will redirect to EVE sign on which will later redirect back 
  * to /eveCallback.  The redirect URL you here should be the same as the one you entered for your
  * registered EVE application, and it should point to your callback request mapping.
  * 
  * @return redirect URL to EVE sign on
  */
 @RequestMapping(value="/launchSignOn", method=RequestMethod.GET)
 public String launchSignOn() {
  return "redirect:https://login.eveonline.com/oauth/authorize"
    + "?response_type=code"
    + "&redirect_uri=" + MY_CLIENT_URL
    + "&client_id=" + MY_CLIENT_ID
    + "&scope=" + SCOPE
    + "&state=" + STATE;
 }
 
 /**
  * EVE sign on should redirect back to here. Now you can send a post request to get an
  * access token (which also includes a refresh token you probably will want to save), and if needed,
  * you can make a request to obtain character details as well (in particular, you will probably
  * want the character ID).
  *  
  * @param model
  * @param code    the code you need to request an access token
  * @param state   your appliction state info, should match what you originally sent in the redirect
  * 
  * @return view page reference, which could display info you want to see about the token or character.
  */
 @RequestMapping(value="/eveCallback", method=RequestMethod.GET)
 public String catchCallback(ModelMap model, @RequestParam String code, @RequestParam String state) {
  // do something with your state if needed
  MoxyJsonConfig moxyJsonConfig = new MoxyJsonConfig();
  ContextResolver<MoxyJsonConfig> jsonConfigResolver = moxyJsonConfig.resolver();
  Feature basicAuth = HttpAuthenticationFeature.basic(MY_CLIENT_ID, MY_SECRET_KEY);
  Client client = ClientBuilder.newBuilder()
    .register(basicAuth)
    .register(jsonConfigResolver)
    .build();
  MultivaluedMap<String, String> formData = new MultivaluedHashMap<String, String>();
  formData.add("grant_type", "authorization_code");
  formData.add("code", code);
  AccessToken accessToken = client.target("https://login.eveonline.com/oauth/token")
    .request(MediaType.APPLICATION_JSON_TYPE)
    .post(Entity.form(formData), new GenericType<AccessToken>(){});
  Feature bearerAuth = OAuth2ClientSupport.feature(accessToken.getAccess_token());
  client = ClientBuilder.newBuilder()
    .register(bearerAuth)
    .register(jsonConfigResolver)
    .build();
  EveCharacter character = client.target("https://login.eveonline.com/oauth/verify")
    .request(MediaType.APPLICATION_JSON_TYPE)
    .get(new GenericType<EveCharacter>(){});
  model.put("accessToken", accessToken);
  model.put("character", character);
  return "showTokenAndCharacterId";
 }
 
 /**
  * Uses a previously stored refresh token to obtain a new access token and character information.
  * 
  * @param model
  * 
  * @return view page reference, could display whatever info you want to see about token or character.
  */
 @RequestMapping(value="/testRefreshToken", method=RequestMethod.GET)
 public String testRefreshToken(ModelMap model) {
  final MoxyJsonConfig moxyJsonConfig = new MoxyJsonConfig();
  final ContextResolver<MoxyJsonConfig> jsonConfigResolver = moxyJsonConfig.resolver();
  Feature basicAuth = HttpAuthenticationFeature.basic(MY_CLIENT_ID, MY_SECRET_KEY);
  Client client = ClientBuilder.newBuilder()
    .register(basicAuth)
    .register(jsonConfigResolver)
    .build();
  MultivaluedMap<String, String> formData = new MultivaluedHashMap<String, String>();  
  formData.add("grant_type", "refresh_token");
  formData.add("refresh_token", MY_REFRESH_TOKEN);
  AccessToken accessToken = client.target("https://login.eveonline.com/oauth/token")
    .request(MediaType.APPLICATION_JSON_TYPE)
    .post(Entity.form(formData), new GenericType<AccessToken>(){});
  Feature bearerAuth = OAuth2ClientSupport.feature(accessToken.getAccess_token());
  client = ClientBuilder.newBuilder()
    .register(bearerAuth)
    .register(jsonConfigResolver)
    .build();
  EveCharacter character = client.target("https://login.eveonline.com/oauth/verify")
    .request(MediaType.APPLICATION_JSON_TYPE)
    .get(new GenericType<EveCharacter>(){});
  model.put("accessToken", accessToken);
  model.put("character", character);
  return "showTokenAndCharacterId";
 }
}

Example Maven Dependencies


  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
  </parent>
  <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>
    <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-webmvc</artifactId>
    </dependency>
    <dependency>
     <groupId>org.apache.tomcat.embed</groupId>
     <artifactId>tomcat-embed-jasper</artifactId> <!-- only needed if using JSP -->
     <scope>provided</scope>
    </dependency>
    <dependency>
     <groupId>org.glassfish.jersey.core</groupId>
     <artifactId>jersey-client</artifactId>
    </dependency>
    <dependency>
     <groupId>org.glassfish.jersey.inject</groupId>
     <artifactId>jersey-hk2</artifactId>
     <version>2.26</version>
    </dependency>
    <dependency>
     <groupId>org.glassfish.jersey.media</groupId>
     <artifactId>jersey-media-moxy</artifactId>
     <version>2.26</version>
    </dependency>
    <dependency>
     <groupId>org.glassfish.jersey.security</groupId>
     <artifactId>oauth2-client</artifactId>
     <version>2.26</version>
    </dependency>
  </dependencies>

3 comments: