読者です 読者をやめる 読者になる 読者になる

謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

自力でFacebookのOAuth2に対応してみた

Scala Playframework 勉強

やることはFacebookアカウントでサイトのアカウント作って、ログイン認証したいだけだったのだが、どうも Playframework2.2 では Pac4J が対応していなかったので、自力で認証してみた。

まずは設定に Facebook のアプリケーション情報を突っ込む

auth {
  facebook {
    app_id="XXXXXXXX"
    app_secret="YYYYYYYYYYYYYYYYYY"
  }
}

そしたらログイン用のコントローラを作成します。名前は適当に。で、Action は必ず routes に突っ込んでください。

object Facebook extends Controller

まず、設定を読み込みます。

  import play.api.Play.current
  val APP_ID     = current.configuration.getString("auth.facebook.app_id").getOrElse("")
  val APP_SECRET = current.configuration.getString("auth.facebook.app_secret").getOrElse("")
  val AUTH_CSRF  = "FACEBOOK_AUTH_CSRF_KEY"

ログイン用のアクションと、そのコールバックを指定します。

def login = Action { implicit request =>
  Ok("Dummy")
}

def callback(hash:String) = Action { implicit request =>
  Ok("Calledback")
}

callback の引数 hash はCSRF対策に使います。

routes に記述したら、まずは login 側から作りこみ

  def getCallbackUrl(csrfToken:String)(implicit request:Request[AnyContent]) = {
    URLEncoder.encode(routes.Facebook.callback(csrfToken).absoluteURL(false), "UTF-8")
  }

  def login = Action { implicit request =>
    val csrfToken = Crypto.generateToken
    val authURL = "http://www.facebook.com/dialog/oauth?client_id=%s&redirect_uri=%s".format(APP_ID, getCallbackUrl(csrfToken))
    Redirect(authURL).withSession(AUTH_CSRF -> csrfToken)
  }

absoluteURL の引数は、本番で HTTPS 居れたら true に設定してください。

やってることは、CSRFトークン混みで、Facebook の認証が通った後のリダイレクト先URLを指定します。

OAuth2の仕様上、本来は https のみで扱う筈ですが、開発中なので今は気にしません。

次はコールバック側。

こちらは、受け取った瞬間に、CSRF をチェックし、正常であれば Facebook からアクセストークンの取得、及び、ユーザ情報を受け取ります。

  def getAccessToken(hash:String)(implicit request:Request[AnyContent]) = {
    val requestCode = request.getQueryString("code").getOrElse("")
    val token_url = "https://graph.facebook.com/oauth/access_token?client_id=" + APP_ID +
      "&redirect_uri=" + getCallbackUrl(hash) +
      "&client_secret=" + APP_SECRET +
      "&code=" + requestCode
    val result = WS.url(token_url).get.map(_.body)
    Await.result(result, 1 second)
  }

  def getUserJson(accessToken:String)(implicit request:Request[AnyContent]) = {
    val token_url = "https://graph.facebook.com/me?" + accessToken
    val result = WS.url(token_url).get.map(_.json)
    Await.result(result, 1 second)
  }

  def csrfCheck[T](urlToken:String, f: => T)(implicit request:Request[AnyContent]) = {
    session.get(AUTH_CSRF).map(csrfToken => {
      if (csrfToken == csrfToken) {
        f
      } else {
        throw new Exception("CSRF error.")
      }
    }).getOrElse(throw new Exception("No session."))
  }

  def callback(hash:String) = Action { implicit request =>
    csrfCheck(hash, {
      val token = getAccessToken(hash)
      val userInfo = getUserJson(token)
      import play.api.libs.json._
      Ok(Json.stringify(userInfo))
    })
  }

成功すると、下記項目がJSONで拾ってこれます。

  • id : 多分数値(ユニーク)
  • name : フルネーム
  • first_name
  • last_name
  • link : ユーザのタイムラインページ
  • birthday : 誕生日
  • hometown : 住所
  • location : 現在座標?
  • work : 会社情報
  • education : 学歴

何も気にせず Facebook のアプリに OK 押すと、これだけの情報が引っこ抜かれるわけで、、、。

お気を付けくださいだわなぁ。

よくあるミス

  • リクエスト先 URL が間違ってる
  • リクエストパラメータに過不足がある(多くてもダメな場合がある)
  • http/https を間違えてる

これに普通に 3h とか悩まされた。こんなもん自分で作るもんじゃないよ!