PlayFramework + pac4jでOAuthを使う　①Twitter認可

# pac4j
javaで作られた認証・認可ライブラリ
多くのサービスとjavaフレームワークに対応している
比較的簡単に実装できる（と思う）
内部で[scribejava](https://github.com/scribejava/scribejava)を使っている模様
play-pac4jには[Deadbolt2](https://github.com/schaloner/deadbolt-2)が統合されており、シームレスにこれを使用できる

- 公式
http://www.pac4j.org/

- GitHub
https://github.com/pac4j/pac4j

- PlayFramework用のモジュール
https://github.com/pac4j/play-pac4j

- PlayFramework(java)+pac4jの公式デモ（実装の参考に）
https://github.com/pac4j/play-pac4j-java-demo

# Play + pac4jエントリー一覧
[PlayFramework + pac4jでOAuthを使う　①Twitter認可](http://qiita.com/skliber/items/ca640e7eaa737ef9246b)
[PlayFramework + pac4jでOAuthを使う　②DB連携](http://qiita.com/skliber/items/7e02b7f287ac5a4fc1f9)

# 本記事のゴール
OAuthを用いてTwitterユーザーを認可する

# 全コード
https://github.com/skliber/pac4j-test

# 実装
## build.sbt
libraryDependenciesにplay-pac4j2.5.0とpac4j-oauth1.9.2を追記

```
libraryDependencies ++= Seq(
  javaJdbc,
  cache,
  javaWs,
  "org.pac4j" % "play-pac4j" % "2.5.0",
  "org.pac4j" % "pac4j-oauth" % "1.9.2"
)

```

## app/modules
### DemoHttpActionAdapter

全てのレスポンスを改変できるクラス
今回はhttpステータスコードが401か403の場合、それぞれ特殊なページをレンダリングするようにした
また、401（認証エラー）の場合にはログインセッションを削除しておく
削除しないと値が空のpac4j用クッキーが作られる

```java:DemoHttpActionAdapter.java
package modules;

import org.pac4j.core.context.HttpConstants;
import org.pac4j.core.context.Pac4jConstants;
import org.pac4j.play.PlayWebContext;
import org.pac4j.play.http.DefaultHttpActionAdapter;
import play.mvc.Result;

import static play.mvc.Results.*;

public class DemoHttpActionAdapter extends DefaultHttpActionAdapter {

	@Override
	public Result adapt(int code, PlayWebContext context) {
		if (code == HttpConstants.UNAUTHORIZED) {
			// セッションを削除
			context.getJavaContext().session().remove(Pac4jConstants.SESSION_ID);
			return unauthorized("401 UNAUTHORIZED");
		} else if (code == HttpConstants.FORBIDDEN) {
			return forbidden("403 FORBIDDEN");
		} else {
			return super.adapt(code, context);
		}
	}
}
```

### SecurityModule

pac4jの設定を行うクラス
実際に動作させるときは、twIdをAPI Keyに、twSecretをAPI Secretの文字列に置き換える

```java:SecurityModule.java
package modules;

import com.google.inject.AbstractModule;
import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.oauth.client.TwitterClient;
import org.pac4j.play.ApplicationLogoutController;
import org.pac4j.play.CallbackController;
import org.pac4j.play.store.PlayCacheStore;
import org.pac4j.play.store.PlaySessionStore;

import play.Configuration;
import play.Environment;

public class SecurityModule extends AbstractModule {

	private final Configuration configuration;

	public SecurityModule(final Environment environment, final Configuration configuration) {
		this.configuration = configuration;
	}

	@Override
	protected void configure() {
		bind(PlaySessionStore.class).to(PlayCacheStore.class);

		// TwitterのAPI KeyとAPI Secretを設定
		TwitterClient twitterClient = new TwitterClient("twId", "twSecret");

		// コールバックURLとクライアントを設定、今回はtwitterのみ
		Clients clients = new Clients("http://localhost:9000/callback", twitterClient);

		Config config = new Config(clients);

		// Authorizer：認証ロジック　複数のAuthorizerをadd可能
		config.addAuthorizer("admin", new RequireAnyRoleAuthorizer<>("ROLE_ADMIN"));

		// ActionAdapterの設定
		config.setHttpActionAdapter(new DemoHttpActionAdapter());

		bind(Config.class).toInstance(config);

		// callback
		CallbackController callbackController = new CallbackController();
		callbackController.setDefaultUrl("/");
		bind(CallbackController.class).toInstance(callbackController);

		// logout
		ApplicationLogoutController logoutController = new ApplicationLogoutController();
		logoutController.setDefaultUrl("/");
		bind(ApplicationLogoutController.class).toInstance(logoutController);
	}
}
```

## app/conf/application.conf

enabled += "modules.SecurityModule"を追記してSecurityModuleを有効化する

```application.conf
...

play.modules {
  # By default, Play will load any class called Module that is defined
  # in the root package (the "app" directory), or you can define them
  # explicitly below.
  # If there are any built-in modules that you want to disable, you can list them here.
  #enabled += my.application.Module
  enabled += "modules.SecurityModule"

  # If there are any built-in modules that you want to disable, you can list them here.
  #disabled += ""
}

...
```

## app/view

### index.scala.html

トップページ
org.pac4j.core.context.Pac4jConstants.SESSION_ID（この定数の値は"pac4jSessionId"）の有無でログイン状態を判定する
未ログインならTwitterLogin用のリンクを表示、ログインしていればLogoutリンクも表示

```index.scala.html
@(message: String)

@main("index") {
    @if(org.pac4j.core.util.CommonHelper.isBlank(session.get(org.pac4j.core.context.Pac4jConstants.SESSION_ID))) {
        <a href="@controllers.routes.LoginController.twitterLogin()">Twitter Login</a>
    }else{
        <a href="@controllers.routes.LoginController.twitterLogin()">Twitter Login</a>
        <br />
        <a href="@org.pac4j.play.routes.ApplicationLogoutController.logout()">Logout</a>
    }
}

```

### protectedIndex.scala.html

ログインしていないと見れないページ
プロファイル（OAuthで認可したユーザーの情報）を受け取って表示
定数で引っ張れるログインセッションの値もついでに表示

```protectedIndex.scala.html
@(profiles: List[org.pac4j.core.profile.CommonProfile])

@main("テストページ") {
    <h1>protected area</h1>
    <a href="..">Back</a><br />
    <br /><br />
    profiles: @profiles<br />
    @if(org.pac4j.core.util.CommonHelper.isBlank(session.get(org.pac4j.core.context.Pac4jConstants.SESSION_ID))) {
        <font color="red">session NOT exist</font>
    }else{
        <font color="red">@session.get(org.pac4j.core.context.Pac4jConstants.SESSION_ID)</font>
    }
}
```

##### ※ pac4jのセッションについて
playframework用のログインセッションはplay-pac4jにあるPlayCacheStoreによって作られる
https://github.com/pac4j/play-pac4j/blob/master/src/main/java/org/pac4j/play/store/PlayCacheStore.java
getOrCreateSessionIdメソッド内でUUIDを生成し、定数Pac4jConstants.SESSION_IDをキーにしてセッションにputしている

## app/controller/LoginController.java

```java:LoginController.java
package controllers;

import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.play.PlayWebContext;
import org.pac4j.play.java.Secure;
import org.pac4j.play.store.PlaySessionStore;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.protectedIndex;

import javax.inject.Inject;
import java.util.List;

public class LoginController extends Controller {

	@Inject
	protected PlaySessionStore playSessionStore;

	private List<CommonProfile> getProfiles() {
		final PlayWebContext context = new PlayWebContext(ctx(), playSessionStore);
		final ProfileManager<CommonProfile> profileManager = new ProfileManager<>(context);
		return profileManager.getAll(true);
	}

	@Secure(clients = "TwitterClient")
	public Result twitterLogin() {
		return ok(protectedIndex.render(getProfiles()));
	}

}

```

#### @Inject protected PlaySessionStore playSessionStore;
セッション操作用のインターフェース
中身にはPlayCacheStoreのインスタンスが入る

#### private List<CommonProfile> getProfiles()
OAuthで認可したユーザーのプロファイルをコンテキストから取得する処理

#### @Secure(clients = "TwitterClient")
Secureアノテーションを付けたメソッドはプロテクトされ、認可、認証済みのユーザーでないと実行できなくなる
未認可状態でアクセスすると認可を求められる
clientsにTwitterClientを記述することで、Twitterアカウントの連携を要求する

## app/conf/routes
indexとprotectedIndexの設定
pac4j用にcallbackとlogoutを設定

```routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# An example controller showing a sample home page
GET     /                           controllers.HomeController.index

#Social
GET     /tw                           		controllers.LoginController.twitterLogin

# pac4j
GET         /callback                         org.pac4j.play.CallbackController.callback()
GET         /logout                           org.pac4j.play.ApplicationLogoutController.logout()

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

```

#動作確認
protectedIndexにアクセスしてTwitterアカウントに連携を許可し、以下のような画面になれば成功
![testpage.png](https://qiita-image-store.s3.amazonaws.com/0/77685/c2b37b31-2d0b-116e-e0f5-99a5eceb49ba.png)


# Next
取得したプロファイル情報をDBに格納し、突合せによるログインログアウトを実装