Spring BootとScribe OAuth Libraryでソーシャルログイン機能を実装する

# 概要

JavaのOAuthライブラリであるScribeJavaを使用してWebアプリケーションに、SNS(Google、Facebook、Twitter)アカウントでソーシャルログインするサンプルアプリケーションです。
ちなみにSpringにはSpring Socialというプロジェクトがありますが、ScribeJavaの方が手軽に実装できそうだったのでこちらで試してみました。

**環境**

* Windows7 (64bit)
* Java 1.8.0_65
* Spring Boot 1.3.0
* ScribeJava 2.0.1

**参考**

* [scribejava] (https://github.com/scribejava/scribejava)
* [Google Developers] (https://developers.google.com/)
* [Facebook開発者 - 開発者向けFacebook] (https://developers.facebook.com/)
* [Twitter Developers] (https://dev.twitter.com/)


## 準備

それぞれのサービスでアプリケーションを登録して認証に使う情報(クライアントIDやクライアントシークレットキーなど)を手に入れます。

### アプリケーションの登録

アプリケーション登録時に重要な項目はCallback URL(Redirect URL)です。ローカル開発環境で試すので登録内容は下記のようになります。

|SNS      |Site URL                |Callback URL (Redirect URL)                |
|:--------|:-----------------------|:------------------------------------------|
|Google   | http://localhost:9000  | http://localhost:9000/google/callback     |
|Facebook | http://localhost:9000  | http://localhost:9000/facebook/callback   |
|Twitter  | 実際にアクセスできるURL  | http://127.0.0.1:9000/twitter/callback    |


#### Google

プロジェクトが作成済み且つ、OAuth同意画面が設定済みという前提で説明を進めます。

[Google Developers console] (https://console.developers.google.com/home)にアクセスしAPI Managerで新しい認証情報を作成します。
図の様に作成する認証情報にOAuthクライアントIDを選択します。

![p01.png](https://qiita-image-store.s3.amazonaws.com/0/22772/be65a059-a1ac-739c-4c57-b2906d99b5ae.png)

アプリケーションの種類に"ウェブアプリケーション"を選択します。
"名前"、"承認済みのJavaScript生成元"、"承認済みのリダイレクトURI"を入力します。

* `承認済みのJavaScript生成元` : `http://localhost:9000`
* `承認済みのリダイレクトURI` : `http://localhost:9000/google/callback`

![p02.png](https://qiita-image-store.s3.amazonaws.com/0/22772/49814324-0fa2-4f33-4ae7-0cdf3f0c4847.png)

(モザイクをかけていますが)図のようにクライアントIDとクライアントシークレットが発行されます。

![p03.png](https://qiita-image-store.s3.amazonaws.com/0/22772/a6a48121-91c6-96e7-8b56-c19002e87c92.png)


#### Facebook

[開発者向けFacebook] (https://developers.facebook.com/)にアクセスし、ページ右上のメニューより"Add a New App"を選択します。

![f01.png](https://qiita-image-store.s3.amazonaws.com/0/22772/72816b23-67db-53e9-b2dd-56085182ddf4.png)

プラットフォームの選択画面で"ウェブサイト"を選択します。

![f02.png](https://qiita-image-store.s3.amazonaws.com/0/22772/6c79104a-dc4d-31ac-515b-ad9bcc414f58.png)

アプリケーション名を入力します。

![f03.png](https://qiita-image-store.s3.amazonaws.com/0/22772/2a94d4b6-20f1-adba-0997-c8f79f8ecc93.png)

アプリケーションのカテゴリを選択します。

![f04.png](https://qiita-image-store.s3.amazonaws.com/0/22772/0aeb9c51-b601-d4bf-ebda-4f49e5318415.png)

TOPページにアクセスしなおして(登録内容が直ぐに反映されないので)、ページ右上のメニューより作成したアプリケーションを選択します。

![f05.png](https://qiita-image-store.s3.amazonaws.com/0/22772/c72a9def-0570-f693-99bb-65e64d7c8b9c.png)

ダッシュボードの左側メニューよりTest Appsを選択し、テストアプリケーションを作成します。

![f07.png](https://qiita-image-store.s3.amazonaws.com/0/22772/fe11b705-6320-7508-e4b9-acaf99f28b7c.png)

テストアプリケーションの名前を入力します。今回はデフォルト値のままで登録しました。

![f08.png](https://qiita-image-store.s3.amazonaws.com/0/22772/fd9cbc48-d462-3f5b-db46-4ade81388a19.png)

SettingsメニューのBasicタブで"+Add Platform"をクリックします。

![f09a.png](https://qiita-image-store.s3.amazonaws.com/0/22772/ba13ff01-016e-59a8-a286-12c373b59b4f.png)

WebSiteを選択します。

![f09b.png](https://qiita-image-store.s3.amazonaws.com/0/22772/541e2f95-7769-e814-19a1-9ded26d2b525.png)

App DomainsとSite URLを入力します。

* `Site URL` : `http://localhost:9000`

![f09c.png](https://qiita-image-store.s3.amazonaws.com/0/22772/281b9d3d-88b5-eb64-463e-6e11d33deb3d.png)

SettingsメニューのAdvancedタブで"Valid OAuth redirect URIs"を入力し、Save Changesボタンをクリックします。
"Valid OAuth redirect URIs"には複数のURIを指定できます。

* `Valid OAuth redirect URIs` : `http://localhost:9000/facebook/callback`

![f10.png](https://qiita-image-store.s3.amazonaws.com/0/22772/e6bba502-79b1-1ca5-2b08-e198a74d73e0.png)

AppIDとApp Secretはダッシュボードで確認できます。

![f11.png](https://qiita-image-store.s3.amazonaws.com/0/22772/d915a571-0a9b-74fc-fbd1-0e16d4e2be7e.png)


#### Twitter

[Twitter Application Management] (https://apps.twitter.com/)にアクセスし、"Create New App"をクリックします。

![b01.png](https://qiita-image-store.s3.amazonaws.com/0/22772/ddc4e8d0-f6f9-48bf-d16f-57ff7bad4fd9.png)

Application Detailsに必要な情報を入力します。
Websiteには、実際にアクセスできるページ(localhostなどのローカルでは駄目なようです)を指定する必要があります。今回はQiitaの自分のプロフィールページを仮に入力しました。
Callback URLにもlocalhostなどは入力できませんが、IPアドレスは入力可能なので`127.0.0.1`としました。

* `Callback URL` : `http://127.0.0.1:9000/twitter/callback`

![b02.png](https://qiita-image-store.s3.amazonaws.com/0/22772/403766ff-dc77-a2c3-9208-67316541b841.png)

Permissionsタブでアクセスタイプを"Read Only"に変更します。

![b04.png](https://qiita-image-store.s3.amazonaws.com/0/22772/76f21fab-011d-99fa-0e2a-dfe075a809b3.png)

Consumer KeyとConsumer Secret(図ではモザイクにしています)はKey and Access Tokensタブで確認できます。

![b03.png](https://qiita-image-store.s3.amazonaws.com/0/22772/459e457b-8888-2f95-5a00-edb5314bddfa.png)


## アプリケーションの開発

使用するScribe OAuthライブラリの依存関係をpom.xmlに追加します。

```xml:scribe
<dependency>
  <groupId>com.github.scribejava</groupId>
  <artifactId>scribejava-apis</artifactId>
  <version>2.0.1</version>
</dependency>
```

また、APIのレスポンスがjsonなのでパースするjsonライブラリも追加します。

```xml:json
<dependency>
  <groupId>org.json</groupId>
  <artifactId>json</artifactId>
  <version>20151123</version>
</dependency>
```

### 雛形アプリケーションを作成

プロジェクト名: `sbsns-example`

**mavenでアプリケーションの雛形を作成**

```text:generate
> mvn archetype:generate -DgroupId=com.example.sbsns -DartifactId=sbsns-example -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
```

```text:eclipse
> cd sbsns-example
> mvn eclipse:eclipse
```

**eclipseにインポート**

* メニューバーの"File" → "Import..." → "Maven" → "Existing Maven Projects"を選択します。
* プロジェクトのディレクトリを選択し、"Finish"ボタンをクリックします。

**pom.xmlの編集**

```xml:pom.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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.sbsns</groupId>
  <artifactId>sbsns-example</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>sbsns-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>com.github.scribejava</groupId>
      <artifactId>scribejava-apis</artifactId>
      <version>2.0.1</version>
    </dependency>

    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20151123</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>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>1.3.0.RELEASE</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <verbose>true</verbose>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
          <compilerArgs>
            <arg>-verbose</arg>
            <arg>-Xlint:all,-options,-path</arg>
          </compilerArgs>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>versions-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
```

**resourcesフォルダの作成**

設定ファイルやテンプレートファイルなどを配置するresourcesフォルダをsrc/main下に作成します。

作成したらプロジェクトに反映させます。

* "Build Path" → "Configure Build Path" → "Java Buld Path" → "Source"タブを選択する。
* "Add Folder"ボタンをクリック → 作成した"resources"フォルダにチェックを入れる。

**application.ymlの作成**

src/main/resourcesフォルダ内にapplication.ymlを作成します。

```yaml:application.yml
# EMBEDDED SERVER CONFIGURATION (ServerProperties)
server:
  port: 9000

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

**logback.xmlの作成**

src/main/resourcesフォルダ内にlogback.xmlを作成します。
ログの出力先フォルダを"D:/logs"に指定しました。

```xml:logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <property name="LOG_DIR" value="D:/logs" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MMM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{35} - %msg %n</pattern>
    </encoder>
  </appender>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
     <file>${LOG_DIR}/sbsns-example.log</file>
     <encoder>
       <charset>UTF-8</charset>
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] - %msg %n</pattern>
     </encoder>
  </appender>

  <logger name="com.example" level="DEBUG" />

  <logger name="org.hibernate" level="ERROR"/>
  <logger name="org.springframework" level="INFO"/>
  <logger name="org.thymeleaf" level="INFO"/>
  <logger name="org.eclipse.jetty" level="INFO"/>
  <logger name="org.apache.http" level="INFO"/>
  <root>
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
  </root>
</configuration>
```

**ビルド**

この時点で動作検証を兼ねてビルドします。

```text:package
> mvn package
```

ビルドが成功したら生成したjarファイルを実行します。
コマンドプロンプトに"Hello World!"と表示されれば成功です。

```text:jar
> cd target
> java -jar sbsns-example-1.0-SNAPSHOT.jar
Hello World!
```

### サンプルアプリケーションの構成

ディレクトリ/ファイル

```text:tree
sbsns-example
 └─src
    └─main
       ├─java
       │  └─com
       │      └─example
       │          └─sbsns
       │              ├─App.java
       │              └─web
       │                 ├─FacebookController.java
       │                 ├─GooglekController.java
       │                 ├─IndexController.java
       │                 └─TwitterController.java
       └─resources
           └─templates
               ├─facebook
               │   └─profile.html
               ├─google
               │   └─profile.html
               ├─twitter
               │   └─profile.html
               └─index.html
```

#### App

```java:App.java
package com.example.sbsns;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
  public static void main( String[] args ) {
    SpringApplication.run(App.class, args);
  }
}
```

#### Top

topページを表示するだけのコントローラーです。
各SNSのsigninページのリンクを表示します。

```java:IndexController.java
package com.example.sbsns.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class IndexController {

  @RequestMapping(value = {"", "/"}, method = RequestMethod.GET)
  public String index() {
    return "index";
  }

}
```

```html:index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
  <title>index</title>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-md-12 well">
        <h3>index</h3>
        <ul>
          <li><a href="/google/signin" th:href="@{/google/signin}">Google signin</a></li>
          <li><a href="/facebook/signin" th:href="@{/facebook/signin}">Facebook signin</a></li>
          <li><a href="/twitter/signin" th:href="@{/twitter/signin}">Twitter signin</a></li>
        </ul>
        <h3>user profile</h3>
        <ul>
          <li><a href="/google/profile" th:href="@{/google/profile}">Google profile</a></li>
          <li><a href="/facebook/profile" th:href="@{/facebook/profile}">Facebook profile</a></li>
          <li><a href="/twitter/profile" th:href="@{/twitter/profile}">Twitter profile</a></li>
        </ul>
      </div>
    </div>
  </div>
</body>
</html>
```

#### Google

```java:GoogleController.java
package com.example.sbsns.web;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

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

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 org.springframework.web.bind.annotation.RequestParam;

import com.github.scribejava.apis.GoogleApi20;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Token;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.model.Verifier;
import com.github.scribejava.core.oauth.OAuthService;

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

  private static final String YOUR_API_KEY = "クライアントID";
  private static final String YOUR_API_SECRET = "クライアントシークレット";

  private static final String HOST = "http://localhost:9000";
  private static final String CALLBACK_URL = "/google/callback";
  private static final Token EMPTY_TOKEN = null;

  //permission scope
  private static final List<String> SCOPES = new ArrayList<String>(){
    private static final long serialVersionUID = 1L;
      {
        add("profile");
        add("email");
      }
  };

  //API End point
  //private static final String PROTECTED_RESOURCE_URL = "https://www.googleapis.com/plus/v1/people/me";
  private static final String USER_PROFILE_API = "https://www.googleapis.com/oauth2/v1/userinfo";
  private static final String QUERY = "?fields=id,name,email";

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

    String secretState = "secret" + new Random().nextInt(999_999);
    request.getSession().setAttribute("SECRET_STATE", secretState);

    OAuthService service = new ServiceBuilder()
      .provider(GoogleApi20.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .scope(String.join(" ", SCOPES))
      .state(secretState)
      .connectTimeout(10)
      .build();

    String redirectURL = service.getAuthorizationUrl(EMPTY_TOKEN);
    logger.info("redirectURL:{}", redirectURL);

    response.sendRedirect(redirectURL);
  }

  @RequestMapping(value ="/callback", method = RequestMethod.GET)
  public String callback(@RequestParam(value = "code", required = false) String code,
      @RequestParam(value = "state", required = false) String state,
      HttpServletRequest request, HttpServletResponse response, Model model) {
    logger.debug("callback");
    logger.info("code:{}", code);
    logger.info("state:{}", state);

    String secretState = (String) request.getSession().getAttribute("SECRET_STATE");

    if (secretState.equals(state)) {
      logger.info("State value does match!");
    } else {
      logger.error("Ooops, state value does not match!");
    }

    OAuthService service = new ServiceBuilder()
      .provider(GoogleApi20.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    String requestUrl = USER_PROFILE_API + QUERY;

    final Verifier verifier = new Verifier(code);
    final Token accessToken = service.getAccessToken(EMPTY_TOKEN, verifier);

    final OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, requestUrl, service);
    service.signRequest(accessToken, oauthRequest);

    final Response resourceResponse = oauthRequest.send();

    logger.info("code:{}", resourceResponse.getCode());
    logger.info("body:{}", resourceResponse.getBody());
    logger.info("message:{}",resourceResponse.getMessage());

    final JSONObject obj = new JSONObject(resourceResponse.getBody());
    logger.info("json:{}" ,obj.toString());

    String googleid = obj.getString("id");
    String name = obj.getString("name");
    String email = obj.getString("email");

    model.addAttribute("id", googleid);
    model.addAttribute("name", name);
    model.addAttribute("email", email);

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

    return "/google/profile";
  }

  @RequestMapping(value ="/profile", method = RequestMethod.GET)
  public String profile(HttpServletRequest request, HttpServletResponse response, Model model) {
    logger.debug("profile");

    OAuthService service = new ServiceBuilder()
      .provider(GoogleApi20.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    final String requestUrl = USER_PROFILE_API + QUERY;
    final Token accessToken = (Token) request.getSession().getAttribute("GOOGLE_ACCESS_TOKEN");

    final OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, requestUrl, service);
    service.signRequest(accessToken, oauthRequest);

    final Response resourceResponse = oauthRequest.send();

    logger.info("code:{}", resourceResponse.getCode());
    logger.info("body:{}", resourceResponse.getBody());
    logger.info("message:{}",resourceResponse.getMessage());

    final JSONObject obj = new JSONObject(resourceResponse.getBody());
    logger.info("json:{}" ,obj.toString());

    String googleid = obj.getString("id");
    String name = obj.getString("name");
    String email = obj.getString("email");

    model.addAttribute("id", googleid);
    model.addAttribute("name", name);
    model.addAttribute("email", email);

    return "google/profile";
  }

}
```

```html:profile.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
  <title>google - profile</title>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-md-12 well">
        <h3>user profile</h3>
        <ul>
          <li th:text="${id}">id</li>
          <li th:text="${name}">name</li>
          <li th:text="${email}">email</li>
        </ul>
        <h3>link</h3>
        <ul>
          <li><a href="/" th:href="@{/}">top</a></li>
        </ul>
      </div>
    </div>
  </div>
</body>
</html>
```

* [Google Sign-In for server-side apps] (https://developers.google.com/identity/sign-in/web/server-side-flow)
* [OAuth 2.0 Scopes for Google APIs] (https://developers.google.com/identity/protocols/googlescopes)

##### Google's OAuth 2.0 endpoint

```text:api
https://accounts.google.com/o/oauth2/auth
```

##### userinfo endpoint

ユーザーのプロフィールを取得するAPI endpointです。

```text:api
https://www.googleapis.com/oauth2/v1/userinfo
```

Google+のプロフィールを取得する場合は下記のendpointを使用します。またdevelopers consoleでGoogle+のAPIを有効化しておく必要があります。

```text:api
https://www.googleapis.com/plus/v1/people/me
```

#### Facebook

```java:FacebookController.java
package com.example.sbsns.web;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

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

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 org.springframework.web.bind.annotation.RequestParam;

import com.github.scribejava.apis.FacebookApi;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Token;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.model.Verifier;
import com.github.scribejava.core.oauth.OAuthService;

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

  private static final String YOUR_API_KEY = "APP ID";
  private static final String YOUR_API_SECRET = "APP SECRET";

  private static final String HOST = "http://localhost:9000";
  private static final String CALLBACK_URL = "/facebook/callback";
  private static final Token EMPTY_TOKEN = null;

  //permission scope
  private static final List<String> SCOPES = new ArrayList<String>(){
    private static final long serialVersionUID = 1L;
      {
        add("public_profile");
        add("user_birthday");
        add("email");
      }
  };

  //API End point
  private static final String USER_PROFILE_API = "https://graph.facebook.com/v2.5/me";
  private static final String QUERY = "?fields=id,name,first_name,last_name,gender,birthday,email";

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

    String secretState = "secret" + new Random().nextInt(999_999);
    request.getSession().setAttribute("SECRET_STATE", secretState);

    OAuthService service = new ServiceBuilder()
      .provider(FacebookApi.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .scope(String.join(",", SCOPES))
      .state(secretState)
      .grantType("code")
      .connectTimeout(10)
      .build();

    String redirectURL = service.getAuthorizationUrl(EMPTY_TOKEN);
    logger.info("redirectURL:{}", redirectURL);

    response.sendRedirect(redirectURL);
  }

  @RequestMapping(value ="/callback", method = RequestMethod.GET)
  public String callback(@RequestParam(value = "code", required = false) String code,
      @RequestParam(value = "state", required = false) String state,
      HttpServletRequest request, HttpServletResponse response, Model model) {
    logger.debug("callback");
    logger.info("code:{}", code);
    logger.info("state:{}", state);

    String secretState = (String) request.getSession().getAttribute("SECRET_STATE");

    if (secretState.equals(state)) {
      logger.info("State value does match!");
    } else {
      logger.error("Ooops, state value does not match!");
    }

    OAuthService service = new ServiceBuilder()
      .provider(FacebookApi.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    final String requestUrl = USER_PROFILE_API + QUERY;

    final Verifier verifier = new Verifier(code);
    final Token accessToken = service.getAccessToken(EMPTY_TOKEN, verifier);

    final OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, requestUrl, service);
    service.signRequest(accessToken, oauthRequest);

    final Response resourceResponse = oauthRequest.send();

    logger.info("code:{}", resourceResponse.getCode());
    logger.info("body:{}", resourceResponse.getBody());
    logger.info("message:{}",resourceResponse.getMessage());

    final JSONObject obj = new JSONObject(resourceResponse.getBody());
    logger.info("json:{}" ,obj.toString());

    String facdebookId = obj.getString("id");
    String name = obj.getString("name");
    String email = obj.getString("email");

    model.addAttribute("id", facdebookId);
    model.addAttribute("name", name);
    model.addAttribute("email", email);

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

    return "facebook/profile";
  }

  @RequestMapping(value ="/profile", method = RequestMethod.GET)
  public String profile(HttpServletRequest request, HttpServletResponse response, Model model) {
    logger.debug("profile");

    OAuthService service = new ServiceBuilder()
      .provider(FacebookApi.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    final String requestUrl = USER_PROFILE_API + QUERY;
    final Token accessToken = (Token) request.getSession().getAttribute("FACEBOOK_ACCESS_TOKEN");

    final OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, requestUrl, service);
    service.signRequest(accessToken, oauthRequest);

    final Response resourceResponse = oauthRequest.send();

    logger.info("code:{}", resourceResponse.getCode());
    logger.info("body:{}", resourceResponse.getBody());
    logger.info("message:{}",resourceResponse.getMessage());

    final JSONObject obj = new JSONObject(resourceResponse.getBody());
    logger.info("json:{}" ,obj.toString());

    String facdebookId = obj.getString("id");
    String name = obj.getString("name");
    String email = obj.getString("email");

    model.addAttribute("id", facdebookId);
    model.addAttribute("name", name);
    model.addAttribute("email", email);

    return "facebook/profile";
  }

}
```

```html:profile.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
  <title>facebook - profile</title>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-md-12 well">
        <h3>user profile</h3>
        <ul>
          <li th:text="${id}">id</li>
          <li th:text="${name}">name</li>
          <li th:text="${email}">email</li>
        </ul>
        <h3>link</h3>
        <ul>
          <li><a href="/" th:href="@{/}">top</a></li>
        </ul>
      </div>
    </div>
  </div>
</body>
</html>
```

##### userinfo endpoint

ユーザーのプロフィールを取得するAPI endpointです。

```text:api
https://graph.facebook.com/v2.5/me
```

* [Permissions Reference - Facebook Login] (https://developers.facebook.com/docs/facebook-login/permissions)


#### Twitter

```java:TwitterController.java
package com.example.sbsns.web;

import java.io.IOException;

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

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 org.springframework.web.bind.annotation.RequestParam;

import com.github.scribejava.apis.TwitterApi;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Token;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.model.Verifier;
import com.github.scribejava.core.oauth.OAuthService;

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

  private static final String YOUR_API_KEY = "Consumer Key";
  private static final String YOUR_API_SECRET = "Consumer Secret";

  private static final String HOST = "http://localhost:9000";
  private static final String CALLBACK_URL = "/twitter/callback";

  //API End point
  private static final String PROTECTED_RESOURCE_URL = "https://api.twitter.com/1.1/account/verify_credentials.json";

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

    OAuthService service = new ServiceBuilder()
      .provider(TwitterApi.Authenticate.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    Token requestToken = service.getRequestToken();
    String redirectURL = service.getAuthorizationUrl(requestToken);
    logger.info("redirectURL:{}", redirectURL);

    response.sendRedirect(redirectURL);
  }

  @RequestMapping(value ="/callback", method = RequestMethod.GET)
  public String callback(@RequestParam(value = "oauth_token", required = false) String oauth_token,
      @RequestParam(value = "oauth_verifier", required = false) String oauth_verifier,
      HttpServletRequest request, HttpServletResponse response, Model model) {
    logger.debug("callback");
    logger.info("oauth_token:{}", oauth_token);
    logger.info("oauth_verifier:{}", oauth_verifier);

    OAuthService service = new ServiceBuilder()
      .provider(TwitterApi.Authenticate.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    final Verifier verifier = new Verifier(oauth_verifier);
    final Token requestToken = new Token(oauth_token, oauth_verifier);
    final Token accessToken = service.getAccessToken(requestToken, verifier);

    final OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL, service);
    service.signRequest(accessToken, oauthRequest);

    final Response resourceResponse = oauthRequest.send();

    logger.info("code:{}", resourceResponse.getCode());
    logger.info("body:{}", resourceResponse.getBody());
    logger.info("message:{}",resourceResponse.getMessage());

    final JSONObject obj = new JSONObject(resourceResponse.getBody());
    logger.info("json:{}" ,obj.toString());

    String twitterId = obj.getString("id_str");
    String name = obj.getString("name");
    String profileImageUrl = obj.getString("profile_image_url");

    model.addAttribute("id", twitterId);
    model.addAttribute("name", name);
    model.addAttribute("imageUrl", profileImageUrl);

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

    return "twitter/profile";
  }

  @RequestMapping(value ="/profile", method = RequestMethod.GET)
  public String profile(HttpServletRequest request, HttpServletResponse response, Model model) {
    logger.debug("profile");

    OAuthService service = new ServiceBuilder()
      .provider(TwitterApi.Authenticate.class)
      .apiKey(YOUR_API_KEY)
      .apiSecret(YOUR_API_SECRET)
      .callback(HOST + CALLBACK_URL)
      .build();

    final Token accessToken = (Token) request.getSession().getAttribute("TWITTER_ACCESS_TOKEN");

    final OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL, service);
    service.signRequest(accessToken, oauthRequest);

    final Response resourceResponse = oauthRequest.send();

    logger.info("code:{}", resourceResponse.getCode());
    logger.info("body:{}", resourceResponse.getBody());
    logger.info("message:{}",resourceResponse.getMessage());

    final JSONObject obj = new JSONObject(resourceResponse.getBody());
    logger.info("json:{}" ,obj.toString());

    String twitterId = obj.getString("id_str");
    String name = obj.getString("name");
    String profileImageUrl = obj.getString("profile_image_url");

    model.addAttribute("id", twitterId);
    model.addAttribute("name", name);
    model.addAttribute("imageUrl", profileImageUrl);

    return "twitter/profile";
  }

}
```

```html:profile.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
  <title>twitter - profile</title>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-md-12 well">
        <h3>user profile</h3>
        <ul>
          <li th:text="${id}">id</li>
          <li th:text="${name}">name</li>
          <li><img th:src="${imageUrl}"></img></li>
        </ul>
        <h3>link</h3>
        <ul>
          <li><a href="/" th:href="@{/}">top</a></li>
        </ul>
      </div>
    </div>
  </div>
</body>
</html>
```

* [Application Permission Model] (https://dev.twitter.com/oauth/overview/application-permission-model)
* [API Rate Limits] (https://dev.twitter.com/rest/public/rate-limiting)


##### [GET oauth/authorize] (https://dev.twitter.com/oauth/reference/get/oauth/authorize)

毎回認証画面が表示されます。

providerに`TwitterApi.class`を指定するとこのAPIが使用されます。

```java
OAuthService service = new ServiceBuilder()
  .provider(TwitterApi.class)
  .apiKey(YOUR_API_KEY)
  .apiSecret(YOUR_API_SECRET)
  .callback(HOST + CALLBACK_URL)
  .build();
```

```text:api
https://api.twitter.com/oauth/authorize
```

##### [GET oauth/authenticate] (https://dev.twitter.com/oauth/reference/get/oauth/authenticate)

一度認証を行えば次から表示されません。

providerに`TwitterApi.Authenticate.class`を指定するとこのAPIが使用されます。

```java
OAuthService service = new ServiceBuilder()
  .provider(TwitterApi.Authenticate.class)
  .apiKey(YOUR_API_KEY)
  .apiSecret(YOUR_API_SECRET)
  .callback(HOST + CALLBACK_URL)
  .build();
```

```text:api
https://api.twitter.com/oauth/authenticate
```

##### [GET account/verify_credentials] (https://dev.twitter.com/rest/reference/get/account/verify_credentials)

ユーザーのプロフィールを取得するAPI endpointです。

```text:api
https://api.twitter.com/1.1/account/verify_credentials.json
```


## メモ

### OAuthについて

* [OAuth2.0の備忘録的まとめ] (http://www.ari-hiro.com/blog/2012/12/30/oauth2-summary)
* [Zusaar開発者に学ぶソーシャルアプリ入門一覧] (http://codezine.jp/article/corner/459)
* [グーグルのAPIを使うときに欠かせないGoogle OAuthの作り方と使い方 (1/3)] (http://www.atmarkit.co.jp/ait/articles/1509/15/news017.html)