Apache Oltuを使ったOAuth認証のサンプルコード

# 概要

Apache Oltuを使ったOAuth認証のサンプルコードです。
このライブラリの検証目的に作成しました。

**環境**

* Windows7 (64bit)
* Java 1.8.0_65
* Spring-Boot 1.3.0
* Apache Oltu 1.0.1

**参考**

* [Apache Oltu] (https://oltu.apache.org/)
* [Working example on OAuth2 + Spring Security + Apache Oltu] (http://yfrankfeng.blogspot.jp/2015/07/working-example-on-oauth2-spring.html)
* [OAuth 2.0 Scopes for Google APIs] (https://developers.google.com/identity/protocols/googlescopes)
* [Using OAuth 2.0 for Web Server Applications] (https://developers.google.com/identity/protocols/OAuth2WebServer)

## サンプルコード

### pom.xml

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example.sbsns.oltu</groupId>
  <artifactId>sbsns-oltu-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>sbsns-oltu-example</name>
  <url>http://maven.apache.org</url>

  <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.0.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-thymeleaf</artifactId>
    </dependency>

    <dependency>
      <groupId>org.apache.oltu.oauth2</groupId>
      <artifactId>org.apache.oltu.oauth2.client</artifactId>
      <version>1.0.1</version>
    </dependency>

    <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-core-asl</artifactId>
      <version>1.9.13</version>
    </dependency>

    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.4</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
```

### application.yml

```text
# EMBEDDED SERVER CONFIGURATION (ServerProperties)
server:
  port: 9000

spring:
# PROFILES
  profiles:
    active: dev
# THYMELEAF (ThymeleafAutoConfiguration)
  thymeleaf:
    enabled: true
    cache: false

facebook:
  app-id: <facebook-app-id>
  app-secret: <faceboo-app-secret>
google:
  app-id: <google-app-id>
  app-secret: <google-app-secret>
````


### Googleのサンプル

#### Config

```java
package com.example.sbsns.oltu;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GoogleConfig {

  @Value("${google.app-id}")
  private String appId;
  @Value("${google.app-secret}")
  private String appSecret;

  public String getAppId() {
    return appId;
  }
  public String getAppSecret() {
    return appSecret;
  }
}
```

#### Form

```java
package com.example.sbsns.oltu.web;

public class GoogleOAuthCallbackForm {

  private String code;
  private String state;
  private String error;

  public String getCode() {
    return code;
  }
  public void setCode(String code) {
    this.code = code;
  }
  public String getState() {
    return state;
  }
  public void setState(String state) {
    this.state = state;
  }
  public String getError() {
    return error;
  }
  public void setError(String error) {
    this.error = error;
  }

  @Override
  public String toString() {
    return "GoogleAuthCallbackForm [code=" + code + ", state=" + state
        + ", error=" + error + "]";
  }

}
```

#### Controller

```java
package com.example.sbsns.oltu.web;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthBearerClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthAccessTokenResponse;
import org.apache.oltu.oauth2.client.response.OAuthAuthzResponse;
import org.apache.oltu.oauth2.client.response.OAuthResourceResponse;
import org.apache.oltu.oauth2.common.OAuthProviderType;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.example.sbsns.oltu.GoogleConfig;
import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
@RequestMapping(value = "/google")
public class GoogleOAuthController {
  private static Logger logger = LoggerFactory.getLogger(GoogleOAuthController.class);

  @Autowired
  private GoogleConfig config;

  private static final String REDIRECT_URI = "http://localhost:9000/google/callback";

  private static final String API_USER_INFO_ENDPOINT = "https://www.googleapis.com/oauth2/v1/userinfo";
  private static final String API_REVOKE_ENDPOINT = "https://accounts.google.com/o/oauth2/revoke";
  //private static final String API_GOOGLE_PLUS_ENDPOINT = "https://www.googleapis.com/plus/v1/people/me";

  @RequestMapping(value = "/signin", method = RequestMethod.GET)
  public void signin(
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws IOException, OAuthSystemException {
    logger.info("signin");

    String state = "abc123xyz456";

    OAuthClientRequest clientRequest = OAuthClientRequest
        .authorizationProvider(OAuthProviderType.GOOGLE)
        .setClientId(config.getAppId())
        .setRedirectURI(REDIRECT_URI)
        .setResponseType("code")
        .setState(state)
        .setScope("https://www.googleapis.com/auth/userinfo.email")
        .buildQueryMessage();

    String redirectURL = clientRequest.getLocationUri();
    logger.info("locationURL:{}", redirectURL);

    response.sendRedirect(clientRequest.getLocationUri());
  }

  @RequestMapping(value = "/callback", method = RequestMethod.GET)
  public String callback(
      GoogleOAuthCallbackForm form,
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws OAuthProblemException, OAuthSystemException {
    logger.info("callback");
    logger.info("form:{}", form);

    String code;
    String state;
    try {
      OAuthAuthzResponse oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request);
      code = oar.getCode();
      state = oar.getState();
      logger.info("code:{}", code);
      logger.info("state:{}", state);
    } catch (OAuthProblemException e) {
      logger.error("OAuth response error", e);
      return "redirect:/";
    }

    OAuthClientRequest oauthRequest = OAuthClientRequest
        .tokenProvider(OAuthProviderType.GOOGLE)
        .setClientId(config.getAppId())
        .setClientSecret(config.getAppSecret())
        .setRedirectURI(REDIRECT_URI)
        .setGrantType(GrantType.AUTHORIZATION_CODE)
        .setCode(code)
        .buildBodyMessage();

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient()); 
    final OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(oauthRequest, "POST");

    String accessToken = oAuthResponse.getAccessToken();
    String scope = oAuthResponse.getScope();
    long expiresIn = oAuthResponse.getExpiresIn();

    logger.info("oAuthResponse:{}", oAuthResponse.toString());
    logger.info("accessToken:{}", accessToken);
    logger.info("scope:{}", scope);
    logger.info("expiresIn:{}", expiresIn);

    request.getSession().setAttribute("accessToken", accessToken);

    return "google/callback";
  }

  @RequestMapping(value = "/user", method = RequestMethod.GET)
  public String user(
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws OAuthSystemException, OAuthProblemException {
    logger.info("user");

    String accessToken = (String) request.getSession().getAttribute("accessToken");

    OAuthClientRequest bearerClientRequest =
        new OAuthBearerClientRequest(API_USER_INFO_ENDPOINT)
              .setAccessToken(accessToken)
              .buildQueryMessage();

    logger.info("locationURL:{}", bearerClientRequest.getLocationUri());

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
    OAuthResourceResponse resourceResponse = oAuthClient.resource(bearerClientRequest, "GET", OAuthResourceResponse.class);

    String userinfoJSON = resourceResponse.getBody();
    logger.info("json:{}", userinfoJSON);

    try {
      GoogleUserProfile userProfile = new ObjectMapper().readValue(userinfoJSON, GoogleUserProfile.class);
      model.addAttribute("userProfile", userProfile);
      logger.info("{}", userProfile.toString());
    } catch (IOException e) {
      logger.error("Json parse error", e);
      return "redirect:/";
    }

    return "google/user";
  }

  @RequestMapping(value = "/signout", method = RequestMethod.GET)
  public String signout(
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws IOException, OAuthSystemException, OAuthProblemException {
    logger.info("signout");

    String accessToken = (String) request.getSession().getAttribute("accessToken");

    OAuthClientRequest bearerClientRequest =
        new OAuthBearerClientRequest(API_REVOKE_ENDPOINT + "?token=" + accessToken)
             .buildQueryMessage();

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
    OAuthResourceResponse resourceResponse = oAuthClient.resource(bearerClientRequest, "GET", OAuthResourceResponse.class);
    String json = resourceResponse.getBody();
    logger.info("json:{}", json);

    return "index";
  }

}
```

#### Response Object

```java
package com.example.sbsns.oltu.web;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown=true)
public class GoogleUserProfile implements Serializable {

  private static final long serialVersionUID = 3543234712791418623L;

  @JsonProperty("id")
  private String id;
  @JsonProperty("name")
  private String name;
  @JsonProperty("gender")
  private String gender;
  @JsonProperty("given_name")
  private String givenName;
  @JsonProperty("family_name")
  private String familyName;
  @JsonProperty("verified_email")
  private boolean verifiedEmail;
  @JsonProperty("email")
  private String email;
  @JsonProperty("picture")
  private String picture;
  @JsonProperty("link")
  private String link;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getGender() {
    return gender;
  }
  public void setGender(String gender) {
    this.gender = gender;
  }
  public String getGivenName() {
    return givenName;
  }
  public void setGivenName(String givenName) {
    this.givenName = givenName;
  }
  public String getFamilyName() {
    return familyName;
  }
  public void setFamilyName(String familyName) {
    this.familyName = familyName;
  }
  public boolean isVerifiedEmail() {
    return verifiedEmail;
  }
  public void setVerifiedEmail(boolean verifiedEmail) {
    this.verifiedEmail = verifiedEmail;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public String getPicture() {
    return picture;
  }
  public void setPicture(String picture) {
    this.picture = picture;
  }
  public String getLink() {
    return link;
  }
  public void setLink(String link) {
    this.link = link;
  }

  @Override
  public String toString() {
    return "GoogleUserProfile [id=" + id + ", name=" + name + ", gender="
        + gender + ", givenName=" + givenName + ", familyName=" + familyName
        + ", verifiedEmail=" + verifiedEmail + ", email=" + email
        + ", picture=" + picture + ", link=" + link + "]";
  }

}
```


### Facebookのサンプル

#### Config

```java
package com.example.sbsns.oltu;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FacebookConfig {

  @Value("${facebook.app-id}")
  private String appID;
  @Value("${facebook.app-secret}")
  private String appSecret;

  public String getAppID() {
    return appID;
  }
  public String getAppSecret() {
    return appSecret;
  }
}
```

#### Form

```java
package com.example.sbsns.oltu.web;

public class FacebookOAuthCallbackForm {

  private String code;
  private String state;
  private String error;
  private String error_code;
  private String error_description;
  private String error_reason;

  public String getCode() {
    return code;
  }
  public void setCode(String code) {
    this.code = code;
  }
  public String getState() {
    return state;
  }
  public void setState(String state) {
    this.state = state;
  }
  public String getError() {
    return error;
  }
  public void setError(String error) {
    this.error = error;
  }
  public String getError_code() {
    return error_code;
  }
  public void setError_code(String error_code) {
    this.error_code = error_code;
  }
  public String getError_description() {
    return error_description;
  }
  public void setError_description(String error_description) {
    this.error_description = error_description;
  }
  public String getError_reason() {
    return error_reason;
  }
  public void setError_reason(String error_reason) {
    this.error_reason = error_reason;
  }

  @Override
  public String toString() {
    return "FacebookAuthCallbackForm [code=" + code + ", state=" + state
        + ", error=" + error + ", error_code=" + error_code
        + ", error_description=" + error_description + ", error_reason="
        + error_reason + "]";
  }

}
```

#### Controller

```java
package com.example.sbsns.oltu.web;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthBearerClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.GitHubTokenResponse;
import org.apache.oltu.oauth2.client.response.OAuthAuthzResponse;
import org.apache.oltu.oauth2.client.response.OAuthResourceResponse;
import org.apache.oltu.oauth2.common.OAuthProviderType;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.example.sbsns.oltu.FacebookConfig;
import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
@RequestMapping(value = "/facebook")
public class FacebookOAuthController {
  private static Logger logger = LoggerFactory.getLogger(FacebookOAuthController.class);

  @Autowired
  private FacebookConfig config;

  private static final String REDIRECT_URI = "http://localhost:9000/facebook/callback";

  @RequestMapping(value = "/signin", method = RequestMethod.GET)
  public void signin(
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws IOException, OAuthSystemException {
    logger.info("signin");

    String state = "abc123xyz456";

    OAuthClientRequest clientRequest = OAuthClientRequest
        .authorizationProvider(OAuthProviderType.FACEBOOK)
        .setClientId(config.getAppID())
        .setRedirectURI(REDIRECT_URI)
        .setResponseType("code")
        .setScope("public_profile,user_friends,user_birthday,email,user_photos,user_about_me")
        .setState(state)
        .buildQueryMessage();

    String redirectURL = clientRequest.getLocationUri();
    logger.info("redirectURL:{}", redirectURL);

    response.sendRedirect(clientRequest.getLocationUri());
  }

  @RequestMapping(value = "/callback", method = RequestMethod.GET)
  public String callback(
      FacebookOAuthCallbackForm form,
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws OAuthProblemException, OAuthSystemException {
    logger.info("callback");
    logger.info("form:{}", form);

    String code;
    String state;
    try {
      OAuthAuthzResponse oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request);
      code = oar.getCode();
      state = oar.getState();
      logger.info("code:{}",code);
      logger.info("state:{}",state);
    } catch (OAuthProblemException e) {
      logger.error("OAuth response error", e);
      return "redirect:/";
    }

    OAuthClientRequest oauthRequest = OAuthClientRequest
        .tokenProvider(OAuthProviderType.FACEBOOK)
        .setClientId(config.getAppID())
        .setClientSecret(config.getAppSecret())
        .setRedirectURI(REDIRECT_URI)
        .setGrantType(GrantType.AUTHORIZATION_CODE)
        .setCode(code)
        .buildBodyMessage();

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
    GitHubTokenResponse oAuthResponse = oAuthClient.accessToken(oauthRequest, GitHubTokenResponse.class);

    String accessToken = oAuthResponse.getAccessToken();
    String scope = oAuthResponse.getScope();
    String expiresIn = oAuthResponse.getParam("expires");

    logger.info("oAuthResponse:{}", oAuthResponse.toString());
    logger.info("accessToken:{}",accessToken);
    logger.info("scope:{}",scope);
    logger.info("expire:{}",expiresIn);

    request.getSession().setAttribute("accessToken", accessToken);

    return "facebook/callback";
  }

  @RequestMapping(value = "/user", method = RequestMethod.GET)
  public String user(
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws OAuthSystemException, OAuthProblemException {
    logger.info("user");

    String accessToken = (String) request.getSession().getAttribute("accessToken");

    OAuthClientRequest bearerClientRequest =
        new OAuthBearerClientRequest("https://graph.facebook.com/me?fields=id,first_name,last_name,picture,email")
            .setAccessToken(accessToken)
            .buildQueryMessage();

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
    OAuthResourceResponse resourceResponse = oAuthClient.resource(bearerClientRequest, "GET", OAuthResourceResponse.class);
    String userinfoJSON = resourceResponse.getBody();
    logger.info("json:{}", userinfoJSON);

    try {
      FacebookUserProfile userProfile = new ObjectMapper().readValue(userinfoJSON, FacebookUserProfile.class);
      request.getSession().setAttribute("facebookId", userProfile.getId());
      model.addAttribute("userProfile", userProfile);
      logger.info("{}", userProfile.toString());
    } catch (IOException e) {
      logger.error("Json parse error", e);
      return "redirect:/";
    }

    return "facebook/user";
  }

  @RequestMapping(value = "/signout", method = RequestMethod.GET)
  public String signout(
      HttpServletRequest request,
      HttpServletResponse response,
      Model model) throws IOException, OAuthSystemException, OAuthProblemException {
    logger.info("signout");

    String accessToken = (String) request.getSession().getAttribute("accessToken");
    String facebookId = (String) request.getSession().getAttribute("facebookId");

    OAuthClientRequest bearerClientRequest =
        new OAuthBearerClientRequest("https://graph.facebook.com/" + facebookId + "/permissions")
            .setAccessToken(accessToken)
            .buildQueryMessage();

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
    OAuthResourceResponse resourceResponse = oAuthClient.resource(bearerClientRequest, "DELETE", OAuthResourceResponse.class);
    String json = resourceResponse.getBody();
    logger.info("json:{}", json);

    return "index";
  }

}
```

#### Response Object

```java
package com.example.sbsns.oltu.web;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown=true)
public class FacebookUserProfile implements Serializable {

  private static final long serialVersionUID = 7957841067903054430L;

  @JsonProperty("id")
  private String id;
  @JsonProperty("last_name")
  private String lastName;
  @JsonProperty("first_name")
  private String firstName;
  @JsonProperty("email")
  private String email;
  @JsonProperty("picture")
  private FacebookPicture picture;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getLastName() {
    return lastName;
  }
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  public String getFirstName() {
    return firstName;
  }
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public FacebookPicture getPicture() {
    return picture;
  }
  public void setPicture(FacebookPicture picture) {
    this.picture = picture;
  }

  @Override
  public String toString() {
    return "FacebookUserProfile [id=" + id + ", lastName=" + lastName
        + ", firstName=" + firstName + ", email=" + email + ", picture="
        + picture + "]";
  }
}
```

```java
package com.example.sbsns.oltu.web;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown=true)
public class FacebookPicture implements Serializable {

  private static final long serialVersionUID = 5591928605444403203L;

  @JsonProperty("data")
  private FacebookPictureData data;

  public FacebookPictureData getData() {
    return data;
  }
  public void setData(FacebookPictureData data) {
    this.data = data;
  }

  @Override
  public String toString() {
    return "FacebookPicture [data=" + data + "]";
  }
}
```

```java
package com.example.sbsns.oltu.web;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown=true)
public class FacebookPictureData implements Serializable {

  private static final long serialVersionUID = -857548118195277215L;

  @JsonProperty("is_silhouette")
  private boolean isSilhouette;
  @JsonProperty("url")
  private String url;

  public boolean isSilhouette() {
    return isSilhouette;
  }
  public void setSilhouette(boolean isSilhouette) {
    this.isSilhouette = isSilhouette;
  }
  public String getUrl() {
    return url;
  }
  public void setUrl(String url) {
    this.url = url;
  }

  @Override
  public String toString() {
    return "FacebookPictureData [isSilhouette=" + isSilhouette + ", url=" + url
        + "]";
  }
}
```