Scala+Play+SecureSocialでTwitterにログイン認証してユーザ名とアバターを表示するメモ


## 使用したバージョン

Scala 2.11.1
Play 2.2.1
SecureSocial 2.1.4

## Playプロジェクトの作成

新規プロジェクトを作成。

```bash:shell
$ play new play221ss214SNSLogin

       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.1 built with Scala 2.10.2 (running Java 1.7.0_67), http://www.playframework.com

The new application will be created in /Users/suzukijun/Documents/Homepage/Chatamp/play221ss214SNSLogin

What is the application name? [play221ss214SNSLogin]
> 

Which template do you want to use for this new application? 

  1             - Create a simple Scala application
  2             - Create a simple Java application

> 1
OK, application play221ss214SNSLogin is created.

Have fun!

$ cd play221ss214SNSLogin/
```

# 各種設定

## plugins.sbtの修正

warファイルを作成できるようにplay2warプラグインを追加しておきます。

```project/plugins.sbt
// Comment to get more information during initialization
logLevel := Level.Warn

// The Typesafe repository
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

// Use the Play sbt plugin for Play projects
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.2.1")

// Use the Play sbt plugin for Play projects
addSbtPlugin("com.github.play2war" % "play2-war-plugin" % "1.2")
```



## build.sbtの修正

mysqlとSecureSocialプラグインを追加。

```build.sbt
import com.github.play2war.plugin._


name := "play221ss214SNSLogin"

version := "1.0-SNAPSHOT"

scalaVersion := "2.11.1"

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache,
  "mysql" % "mysql-connector-java" % "5.1.18",
  "ws.securesocial" %% "securesocial" % "2.1.4"
)     

play.Project.playScalaSettings

Play2WarPlugin.play2WarSettings

Play2WarKeys.servletVersion := "3.0"

```

## play.pluginsファイルの追加

```conf/play.plugins
9994:securesocial.core.DefaultAuthenticatorStore
9995:securesocial.core.DefaultIdGenerator
9996:securesocial.core.providers.utils.DefaultPasswordValidator
9997:securesocial.controllers.DefaultTemplatesPlugin
9998:service.MyUserService
9999:securesocial.core.providers.utils.BCryptPasswordHasher
10000:securesocial.core.providers.TwitterProvider
#10004:securesocial.core.providers.UsernamePasswordProvider
```

## routesファイルの修正

```conf/routes
# Home page
GET     /                           controllers.Application.index

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


# Login page
GET     /login                      securesocial.controllers.LoginPage.login
GET     /logout                     securesocial.controllers.LoginPage.logout

# User Registration and password handling 
GET     /signup                     securesocial.controllers.Registration.startSignUp
POST    /signup                     securesocial.controllers.Registration.handleStartSignUp
GET     /signup/:token              securesocial.controllers.Registration.signUp(token)
POST    /signup/:token              securesocial.controllers.Registration.handleSignUp(token)
GET     /reset                      securesocial.controllers.Registration.startResetPassword
POST    /reset                      securesocial.controllers.Registration.handleStartResetPassword
GET     /reset/:token               securesocial.controllers.Registration.resetPassword(token)
POST    /reset/:token               securesocial.controllers.Registration.handleResetPassword(token)
GET     /password                   securesocial.controllers.PasswordChange.page
POST    /password                   securesocial.controllers.PasswordChange.handlePasswordChange

# Providers entry points
GET     /authenticate/:provider     securesocial.controllers.ProviderController.authenticate(provider)
POST    /authenticate/:provider     securesocial.controllers.ProviderController.authenticateByPost(provider)
GET     /not-authorized             securesocial.controllers.ProviderController.notAuthorized
```


## application.confファイルの修正

１行目に以下の行を追加

```conf/application.conf
include "securesocial.conf"
```

MySQLの設定
>ebdbとなっているところは、データベース名をいれてください。

 

```conf/application.conf
db.ebdb.driver="com.mysql.jdbc.Driver"
db.ebdb.url="jdbc:mysql://localhost/ebdb"
db.ebdb.user="[データベースのユーザー名]"
db.ebdb.pass="[データベースのパスワード]"
#実行されたSQLを出力するように指定
db.ebdb.logStatements=true
```

## securesocial.confファイルを追加

Consumer KeyとConsumer Secretは[Twitterにアプリを登録](https://dev.twitter.com/)して取得しておきます。

```conf/securesocial.conf
securesocial {
    onLoginGoTo=/
    onLogoutGoTo=/login
    ssl=false	

	twitter {
    	requestTokenUrl="https://api.twitter.com/oauth/request_token"
    	accessTokenUrl="https://api.twitter.com/oauth/access_token"
    	authorizationUrl="https://api.twitter.com/oauth/authorize"
		consumerKey="[Your Twitter Consumer Key]"
		consumerSecret="[Your Twitter Consumer Secret]"
	}
}
```


## マイグレーションの設定：1.sql

ebdbとなっているところはご自分の環境にあわせて変更してくださ。

>conf/evolutions/ の後に続く部分はdatabase名になります。

```sql:conf/evolutions/ebdb/1.sql
# Ups以下はマイグレート時に実行されるSQL
# --- !Ups                                                                                                                                                                          

    CREATE TABLE user
    (
        id INT(12) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT,
        name VARCHAR(200) NOT NULL UNIQUE,
        first_name VARCHAR(200),
        last_name VARCHAR(200),
        full_name VARCHAR(200),
        password VARCHAR(200),
        avator_url VARCHAR(200),
        email VARCHAR(200),
        provider enum ('twitter', 'facebook', 'google', 'github', 'userpass', 'linkedin', 'foursquare', 'dropbox', 'xing', 'instagram', 'vk', 'weibo') NOT NULL,
        auth_method VARCHAR(50),
        oauth1token VARCHAR(200),
        oauth1secret VARCHAR(200),
        oauth2access_token VARCHAR(200),
        oAuth2TokenType VARCHAR(200),
        oAtuh2ExpiresIn INT,
        oAuth2RefreshToken VARCHAR(200),
        sex enum ('male', 'female', 'other') default 'other',
        description TEXT,

        created DATETIME NOT NULL,
        modified DATETIME NOT NULL,

        PRIMARY KEY (id)
    ) CHARSET utf8 COMMENT = 'User Accounts';

    /* Create Indexes */
    CREATE UNIQUE INDEX USER_IDX ON user (name ASC);
    CREATE INDEX ACCOUNT_IDX ON user (id ASC);


# Downs以下はマイグレート差し戻し時に実行されるSQL
# --- !Downs

	DROP INDEX USER_IDX ON user;
	DROP INDEX ACCOUNT_IDX ON user;

	DROP TABLE user;
```

# Controllerの修正

## Application.scalaの修正

```app/controllers/Application.scala
package controllers

import play.api._
import play.api.mvc._
import securesocial.core._

object Application extends securesocial.core.SecureSocial {

  def index = SecuredAction { implicit request =>
    // ここにくる時点でfullNameやアクセストークンは取得済み                                                                           
    val name = request.user.fullName
    val provider = request.user.identityId.providerId

    // ためしにアクセストークンとアクセスシークレットを取得してみる。                                                     
    var token = ""
    val oAuth1Info:Option[OAuth1Info] = request.user.oAuth1Info
    oAuth1Info match {
      case Some(info) => {
        token = info.token
        val secret = info.secret
      }
      case None => ()
    }

    Ok(views.html.index(name, provider, request.user.avatarUrl, token))
  }

  def page = UserAwareAction { implicit request =>
    val userName = request.user match {
      case Some(user) => user.fullName
      case _ => "guest"
    }
    Ok("Hello %s".format(userName))
  }

}
```

# Modelの作成

## Userクラスの作成

```scala:app/models/User.scala
package models

import java.io.Serializable
import securesocial.core._
import securesocial.core.providers._
import anorm._ 
import anorm.SqlParser._

import play.api.db._
import play.api.Play.current// DB.withConnectionで必要


case class User(
    id:Pk[Long],
    name:String,
    provider:String,
    email:Option[String],
    passwordInfo:Option[PasswordInfo],
    first_name:Option[String],
    last_name:Option[String],
    full_name:Option[String],
    avatarUrl:Option[String],
    authMethod: securesocial.core.AuthenticationMethod,
    oAuth1Info: Option[securesocial.core.OAuth1Info],
    oAuth2Info: Option[securesocial.core.OAuth2Info]) extends Identity
{

  def isEmpty : Boolean = {
      id != NotAssigned
      }
  
  def identityId: securesocial.core.IdentityId = {
      IdentityId(name, provider)
  }
  
  def firstName:String = {
      if(first_name == null) return ""

      first_name match {
	    case Some(s) => s
	      case None => ""
      }
  }
  
  def lastName:String = {
      if(last_name == null) return ""

      last_name match {
	    case Some(s) => s
	      case None => ""
      }
  }

  def fullName: String = {
      if(full_name == null){
	    ""
      }else{
	    full_name match {
	            case Some(s) => s
		      case None => ""
	    }
      }
  }

}

object User {
  
   val parser: RowParser[User] = {
    get[Pk[Long]]("user.id") ~
    get[String]("user.name") ~
    get[String]("user.provider") ~
    get[Option[String]]("user.email") ~
    get[Option[String]]("user.password") ~
    get[Option[String]]("user.first_name") ~
    get[Option[String]]("user.last_name") ~
    get[Option[String]]("user.full_name") ~
    get[Option[String]]("user.avator_url") ~
    get[String]("user.auth_method") ~
    get[String]("user.oauth1token") ~
    get[String]("user.oauth1secret") ~
    get[String]("user.oauth2access_token") ~
    get[Option[String]]("user.oAuth2TokenType") ~
    get[Option[Int]]("user.oAtuh2ExpiresIn") ~
    get[Option[String]]("user.oAuth2RefreshToken") map {
      case id ~ name ~ provider ~ email ~ password ~ first_name ~ last_name ~ full_name ~ 
           avatorUrl ~ auth_method ~ oauth1token ~ oauth1secret ~ oauth2access_token ~ tokenType ~ expiresIn ~ oAuth2RefreshToken => 
        val pass = password match {
          case Some(s) => {
            id match {
              case NotAssigned => None
              case Id(num) => Some(new PasswordInfo(num.toString(), s))
            }
          }
          case None => None
        }
        
        val authMethod = auth_method match {
          case "OAuth1" => AuthenticationMethod.OAuth1
          case _        => AuthenticationMethod.OAuth2
        }
        val oAuth1Info = if(oauth1token == null || oauth1token.length == 0) None else Some(OAuth1Info(oauth1token, oauth1secret))
        val oAuth2Info = if(oauth2access_token == null || oauth2access_token.length == 0)
			   None
			 else
			   Some(OAuth2Info(oauth2access_token, null, null, null))
        User(id, name, provider, email, pass, first_name, last_name, full_name, avatorUrl, authMethod, oAuth1Info, oAuth2Info)
    }
   }


  def findByName(name:String, provider:String) : Option[User] = {
    println("User.findByName. name:%s, provider: %s".format(name, provider))

    provider match {
      case "userpass" => {  // mail & password login
	DB.withConnection("ebdb"){ implicit connection =>
	    val user = SQL("select * from user where name = {name}").on('name -> name).as(User.parser.singleOpt)
	    println("email.  user = " + user)
	    user
	}
      }
      case _ => {  // SNS login
        println("SNS Login: findByName")

	DB.withConnection("ebdb"){ implicit connection =>
	    val user = SQL("select * from user where name = {name}").on('name -> name).as(User.parser.singleOpt)
	    println("SNS user = " + user)
	    user
	}
      }
    }
    
  }

  def findById(id: Long): Option[User] = {
    println("User.findById")

    DB.withConnection("ebdb") { implicit connection =>
      SQL("select * from user where id = {id}").on('id -> id).as(User.parser.singleOpt)
    }
  }

  def findByEmailAndProvider(email: String, providerId: String): Option[User] = {
    println("User.findByEmailAndName")

    DB.withConnection("ebdb") { implicit connection =>
      SQL("select * from user where email = {email} and provider = {providerId}").on('email -> email, 'providerId -> providerId).as(User.parser.singleOpt)
    }
  }

  def insert(i : Identity) : Option[User] = {
    val provider = i.identityId.providerId
    val name = i.identityId.userId
    val authMethod = i.authMethod
    val avatarUrl = i.avatarUrl
    val firstName = i.firstName
    val fullName = i.fullName
    val lastName = i.lastName
    val oAuth1Info = i.oAuth1Info
    val oAuth2Info = i.oAuth2Info
    val email = i.email
    val passwordOpt = i.passwordInfo
    val password = passwordOpt match {
      case Some(pass) => Some(pass.password)
      case None => None
    }

    oAuth1Info match {
      case Some(info) => println("oAuth1 token" + info.token + ", secret = " + info.secret)
      case None => println("no oAuth1")
    }
    oAuth2Info match {
      case Some(info) => println("oAuth2 accesstoken = " + info.accessToken )
      case None => println("no oAuth2")
    }
    
    val (token, secret) = oAuth1Info match {
      case Some(info) => (info.token, info.secret)
      case None => ("", "")
    }
    val (accessToken, tokenType, expiresIn, refreshToken):(String, Option[String], Option[Int], Option[String]) = oAuth2Info match {
          case Some(info) => (info.accessToken, info.refreshToken, info.expiresIn, info.tokenType)
          case None => ("", None, None, None)
    }
    
    insert(name,
           password,
           provider,
           email,
           authMethod,
           avatarUrl,
           firstName,
           lastName,
           fullName,
           token,
           secret,
           accessToken,
           tokenType,
           expiresIn,
           refreshToken)
  }
  
  def createNewUser(i: Identity) : Option[User] = {
    println("User.createNewUser")

    insert(i)
  }

  def insert(name:String,
      password:Option[String],
      provider:String,
      emailOpt:Option[String],
      authMethod:AuthenticationMethod,
      avatarUrlOpt:Option[String],
      firstNameNullable:String,
      lastNameNullable:String,
      fullNameNullable:String,
      oAuth1TokenNullable:String,
      oAuth1SecretNullable:String,
      oAuth2AccessTokenNullable:String,
      oAuth2TokenType:Option[String],
      oAtuh2ExpiresIn:Option[Int],
      oAuth2RefreshToken:Option[String]
  ) : Option[User] =
  {
    println("User.insert main")

    var user : Option[User] = findByName(name, provider)
    
    user match {
      case Some(_) => return user
      case None => () // do nothing
    }

    DB.withConnection("ebdb"){ implicit conn =>
      var id:Option[Long] = None
      
      val email = emailOpt.getOrElse("")
      val avatarUrl = avatarUrlOpt.getOrElse("")
      val firstName = if(firstNameNullable == null) None else Some(firstNameNullable)
      val lastName =  if(lastNameNullable == null) None else Some(lastNameNullable)
      val fullName = if(fullNameNullable == null) None else Some(fullNameNullable)
      val oAuth1Token = if(oAuth1TokenNullable == null) None else Some(oAuth1TokenNullable)
      val oAuth1Secret = if(oAuth1SecretNullable == null) None else Some(oAuth1SecretNullable)
      val oAuth2AccessToken = if(oAuth2AccessTokenNullable == null) None else Some(oAuth2AccessTokenNullable)


      val sqlQuery = SQL("""
                insert into user (name, password, provider, email, first_name, last_name, full_name, avator_url,
                                  auth_method, oauth1token, oauth1secret, oauth2access_token, oAuth2TokenType, oAtuh2ExpiresIn, oAuth2RefreshToken, created, modified)
                values ({name}, {password}, {provider}, {email}, {firstName}, {lastName}, {fullName}, {avatarUrl},
                        {authMethod}, {oAuth1Token}, {oAuth1Secret}, {oAuth2AccessToken}, {oAuth2TokenType}, {oAtuh2ExpiresIn}, {oAuth2RefreshToken}, now(), now());
			""")
      id = sqlQuery.on(
	    'name -> name,
	    'password -> password,
	    'provider -> provider,
	    'email -> email,
	    'firstName -> firstName,
	    'lastName -> lastName,
	    'fullName -> fullName,
	    'avatarUrl -> avatarUrl,
	    'authMethod -> authMethod.toString(),
	    'oAuth1Token -> oAuth1Token,
	    'oAuth1Secret -> oAuth1Secret,
	    'oAuth2AccessToken -> oAuth2AccessToken,
	    'oAuth2TokenType -> oAuth2TokenType,
	    'oAtuh2ExpiresIn -> oAtuh2ExpiresIn,
	    'oAuth2RefreshToken -> oAuth2RefreshToken
      ).executeInsert()
        
      println("SQL Insert id " + id)
      
      val oAuth1Info = if(oAuth1TokenNullable == null) None else Some(OAuth1Info(oAuth1TokenNullable, oAuth1SecretNullable))
      val oAuth2Info = if(oAuth2AccessTokenNullable == null) None else Some(OAuth2Info(oAuth2AccessTokenNullable, oAuth2TokenType, oAtuh2ExpiresIn, oAuth2RefreshToken))


      id match {
        case Some(num:Long) => {
          password match {
            case None => user = Some(new User(new Id(num), name, provider, emailOpt, None, firstName, lastName, fullName, avatarUrlOpt, authMethod, oAuth1Info, oAuth2Info))
            case Some(s) => {
              val pass = new PasswordInfo(num.toString(), s) // hasher:このパスワードをhashするのに本人のidを使用している
              user = Some(new User(new Id(num), name, provider, emailOpt, Some(pass), firstName, lastName, fullName, avatarUrlOpt, authMethod, oAuth1Info, oAuth2Info))
            }
          }
        }
        case None => {
          println("db user = None")
          user = None
        }
      }
    } // end of DB.withConnection

    user
  }
}
```


## MyUserService.scalaの作成

```app/service/MyUserService.scala
package service

import play.api.{Logger, Application}

import securesocial.core._
import securesocial.core.providers.Token
import securesocial.core.IdentityId

import models.User;

case class MySocialUser(localUser: User,
                        identityId: IdentityId,
                        firstName: String,
                        lastName: String,
                        fullName: String,
                        email: Option[String],
                        avatarUrl: Option[String],
                        authMethod: AuthenticationMethod,
                        oAuth1Info: Option[OAuth1Info] = None,
                        oAuth2Info: Option[OAuth2Info] = None,
                        passwordInfo: Option[PasswordInfo] = None) extends Identity

object MySocialUser {
  def apply(user: User): MySocialUser = {
    MySocialUser(
        user,
        IdentityId(user.name,user.provider),
        user.firstName,
        user.lastName,
        user.fullName,
        user.email,
        user.avatarUrl,
        user.authMethod,
        user.oAuth1Info,
        user.oAuth2Info,
        user.passwordInfo)
  }
}


class MyUserService(application: Application) extends UserServicePlugin(application) {
  private var tokens = Map[String, Token]()

  def find(id: IdentityId): Option[Identity] = {
    User.findByName(id.userId,id.providerId);
  }

  def findByEmailAndProvider(email: String, providerId: String): Option[Identity] = {
    User.findByEmailAndProvider(email, providerId)
  }

  def save(i: Identity) = {
    val ii=i.identityId
    var user = User.findByName(ii.userId,ii.providerId);
    user match {
      case Some(u) => {
        user = User.insert(u)
        MySocialUser(user.get)
      }
      case None => {
        println("save None Profile")
        user = User.createNewUser(i)
        MySocialUser(user.get)
      }
    }
  }

  def save(token: Token) {
    println("save(token:) token = " + token)
    
    tokens += (token.uuid -> token)
  }

  def findToken(token: String): Option[Token] = {
    tokens.get(token)
  }

  def deleteToken(uuid: String) {
    tokens -= uuid
  }

  def deleteTokens() {
    tokens = Map()
  }

  def deleteExpiredTokens() {
    tokens = tokens.filter(!_._2.isExpired)
  }
}
```

# カスタムViewの作成

### index.scala.htmlの修正

```html:app/views/index.scala.html
@(user: String, provider: String, avatarUrl: Option[String], token: String)(implicit request: RequestHeader, lang: Lang)

@import helper._
@import securesocial.core.Registry
@import securesocial.core.AuthenticationMethod._
@import securesocial.core.providers.UsernamePasswordProvider.UsernamePassword

@main("Welcome") {
  <div class="page-header">
    <h1>Welcome!</h1>
  </div>

  Welcome @(user)@@@(provider)!

  @avatarUrl match {
    case Some(url) => { <image src="@url" /> }
    case None => {}
  }

  <p>
    Your access token is '@token'
  </P>

  <p>
    <a href="/logout">Logout</a>
  </p>
}
```

## 実行

ここまでで動作するはずですので、実行してみます。

```bash:
$ play run
```


ブラウザで http://localhost:9000/ に接続してうまくツイッターでログイン認証できれば成功です。