Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vkontakte provider and example of usage #170

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ object ApplicationBuild extends Build {
name := appName + "-social",
libraryDependencies += "com.typesafe.play" %% "play" % playVersion % "provided",
libraryDependencies += "com.typesafe.play" %% "play-ws" % playVersion % "provided",
libraryDependencies += "com.fasterxml.jackson.module" % "jackson-module-scala_2.10" % "2.7.2",
libraryDependencies += "net.minidev" % "json-smart" % "1.0.9",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you introduce jackson and json-smart?

I want to decrease dependent libraries as possible.

Would you use play-json instead of it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I'll try :)

publishMavenStyle := appPublishMavenStyle,
publishArtifact in Test := appPublishArtifactInTest,
pomIncludeRepository := appPomIncludeRepository,
Expand Down
60 changes: 51 additions & 9 deletions social-sample/app/controllers/Application.scala
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package controllers

import jp.t2v.lab.play2.auth.social.providers.twitter.{TwitterController, TwitterProviderUserSupport}
import jp.t2v.lab.play2.auth.social.providers.facebook.{FacebookController, FacebookProviderUserSupport}
import jp.t2v.lab.play2.auth.social.providers.github.{GitHubController, GitHubProviderUserSupport}
import jp.t2v.lab.play2.auth.social.providers.slack.SlackController
import jp.t2v.lab.play2.auth.social.providers.vkontakte.{VkontakteController, VkontakteProviderUserSupport}
import jp.t2v.lab.play2.auth._
import models._
import play.api.mvc.Results._
import play.api.mvc._
import scalikejdbc.DB
import scalikejdbc._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ ExecutionContext, Future }
import scala.reflect.{ ClassTag, classTag }
import jp.t2v.lab.play2.auth._
import jp.t2v.lab.play2.auth.social.providers.twitter.{TwitterProviderUserSupport, TwitterController}
import jp.t2v.lab.play2.auth.social.providers.facebook.{FacebookProviderUserSupport, FacebookController}
import jp.t2v.lab.play2.auth.social.providers.github.{GitHubProviderUserSupport, GitHubController}
import jp.t2v.lab.play2.auth.social.providers.slack.SlackController
import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.{ClassTag, classTag}


object Application extends Controller with OptionalAuthElement with AuthConfigImpl with Logout {

Expand All @@ -23,7 +25,8 @@ object Application extends Controller with OptionalAuthElement with AuthConfigIm
val facebookUser = user.flatMap(u => FacebookUser.findByUserId(u.id))
val twitterUser = user.flatMap(u => TwitterUser.findByUserId(u.id))
val slackAccessToken = user.flatMap(u => SlackAccessToken.findByUserId(u.id))
Ok(views.html.index(user, gitHubUser, facebookUser, twitterUser, slackAccessToken))
val vkontakteUser = user.flatMap(u => VkontakteUser.findByUserId(u.id))
Ok(views.html.index(user, gitHubUser, facebookUser, twitterUser, slackAccessToken,vkontakteUser))
}
}

Expand Down Expand Up @@ -170,3 +173,42 @@ object SlackAuthController extends SlackController
}

}


object VkontakteAuthController extends VkontakteController
with AuthConfigImpl
with VkontakteProviderUserSupport {

override def onOAuthLinkSucceeded(token: AccessToken, consumerUser: User)(implicit request: RequestHeader, ctx: ExecutionContext): Future[Result] = {
retrieveProviderUser(token).map { providerUser =>
DB.localTx { implicit session =>
VkontakteUser.save(consumerUser.id, providerUser)
Redirect(routes.Application.index)
}
}
}

override def onOAuthLoginSucceeded(token: AccessToken)(implicit request: RequestHeader, ctx: ExecutionContext): Future[Result] = {
retrieveProviderUser(token).flatMap { providerUser =>
DB.localTx { implicit session =>
VkontakteUser.findById(providerUser.id) match {
case None =>
val id = User.create(providerUser.first_name, providerUser.coverUrl).id
VkontakteUser.save(id, providerUser)
gotoLoginSucceeded(id)
case Some(fu) =>
gotoLoginSucceeded(fu.userId)
}
}
}
}


def onAuthSucces(): Unit = {

}


}


40 changes: 39 additions & 1 deletion social-sample/app/models/User.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package models

import jp.t2v.lab.play2.auth.social.providers
import jp.t2v.lab.play2.auth.social.providers.{facebook, twitter,vkontakte}
import scalikejdbc._
import jp.t2v.lab.play2.auth.social.providers.{facebook, twitter}

sealed trait Authority
case object Admin extends Authority
Expand Down Expand Up @@ -35,6 +35,15 @@ case class TwitterUser(
case class SlackAccessToken(
userId: Long,
accessToken: String)


case class VkontakteUser(
userId: Long,
id: String,
name: String,
coverUrl: String,
accessToken: String)

object User {

def *(rs: WrappedResultSet) = User(
Expand Down Expand Up @@ -165,4 +174,33 @@ object SlackAccessToken {
SlackAccessToken(userId, accessToken)
}

}

object VkontakteUser {

def *(rs: WrappedResultSet) = VkontakteUser(
rs.long("user_id"),
rs.string("id"),
rs.string("name"),
rs.string("cover_url"),
rs.string("access_token")
)

def findById(id: String)(implicit session: DBSession): Option[VkontakteUser] = {
sql"SELECT * FROM vkontakte_users WHERE id = $id".map(*).single().apply()
}

def findByUserId(userId: Long)(implicit session: DBSession): Option[VkontakteUser] = {
sql"SELECT * FROM vkontakte_users WHERE user_id = $userId".map(*).single().apply()
}

def save(userId: Long, vkontakteUser: vkontakte.VkontakteUser)(implicit session: DBSession): VkontakteUser = {
val id = vkontakteUser.id
val first_name = vkontakteUser.first_name
val coverUrl = vkontakteUser.coverUrl
val accessToken = vkontakteUser.accessToken
sql"INSERT INTO vkontakte_users(user_id, id, name, cover_url, access_token) VALUES ($userId, $id, $first_name, $coverUrl, $accessToken)".update.apply()
VkontakteUser(userId, id, first_name, coverUrl, accessToken)
}

}
21 changes: 20 additions & 1 deletion social-sample/app/views/index.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
gitHubUser: Option[GitHubUser],
facebookUser: Option[FacebookUser],
twitterUser: Option[TwitterUser],
slackAccessToken: Option[SlackAccessToken])
slackAccessToken: Option[SlackAccessToken],
vkontakteUser: Option[VkontakteUser])

<html>
<head>
Expand Down Expand Up @@ -98,6 +99,21 @@ <h2>Slack</h2>
}
</div>

<div class="provider">
<h2>Vkontakte</h2>

@vkontakteUser.map { u =>
<p>
@u.name
</p>
<p>
<img src="@{u.coverUrl}">
</p>
}.getOrElse {
<a href="@routes.VkontakteAuthController.link("email")">link Vkontakte</a>
}
</div>

</div>

}.getOrElse {
Expand All @@ -110,6 +126,9 @@ <h2>Slack</h2>
<p>
<a href="@routes.FacebookAuthController.login("email")">facebook login</a>
</p>
<p>
<a href="@routes.VkontakteAuthController.login("email")">vkontakte login</a>
</p>
}
</div>
</body>
Expand Down
4 changes: 4 additions & 0 deletions social-sample/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ slack.clientId = ${SLACK_CLIENT_ID}
slack.clientSecret = ${SLACK_CLIENT_SECRET}
slack.callbackURL = ${SLACK_CALLBACK_URL}

vkontakte.clientId = ${VKONTAKTE_CLIENT_ID}
vkontakte.clientSecret = ${VKONTAKTE_CLIENT_SECRET}
vkontakte.callbackURL =${VKONTAKTE_CALLBACK_URL}

logger.com.github.tototoshi.play.social = DEBUG

db.default.driver=org.h2.Driver
Expand Down
8 changes: 8 additions & 0 deletions social-sample/conf/db/migration/default/V1__create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,12 @@ CREATE TABLE facebook_users (
CREATE TABLE slack_access_token (
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id),
access_token VARCHAR(1000) NOT NULL
);

CREATE TABLE vkontakte_users (
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id),
id VARCHAR(100) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
cover_url VARCHAR(1000) NOT NULL,
access_token VARCHAR(1000) NOT NULL
);
6 changes: 5 additions & 1 deletion social-sample/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ GET /link/facebook controllers.FacebookAuthController.link(sc
GET /authorize/facebook controllers.FacebookAuthController.authorize

GET /link/slack controllers.SlackAuthController.link(scope: String)
GET /authorize/slack controllers.SlackAuthController.authorize
GET /authorize/slack controllers.SlackAuthController.authorize

GET /login/vkontakte controllers.VkontakteAuthController.login(scope: String)
GET /link/vkontakte controllers.VkontakteAuthController.link(scope: String)
GET /authorize/vkontakte controllers.VkontakteAuthController.authorize
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package jp.t2v.lab.play2.auth.social.providers.vkontakte

/**
* Created by Yuri Rastegaev on 19.03.2016.
*/

import java.net.URLEncoder

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import jp.t2v.lab.play2.auth.social.core.{AccessTokenRetrievalFailedException, OAuth2Authenticator}
import play.api.Logger
import play.api.Play.current
import play.api.http.{HeaderNames, MimeTypes}
import play.api.libs.ws.{WS, WSResponse}
import play.api.mvc.Results

import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal


class VkontakteAuthenticator extends OAuth2Authenticator {

type AccessToken = String

val providerName: String = "vkontakte"

val accessTokenUrl = "https://oauth.vk.com/access_token"

val authorizationUrl = "https://oauth.vk.com/authorize"

val display = "page"

val response_type = "code"

lazy val clientId = current.configuration.getString("vkontakte.clientId").getOrElse(sys.error("vkontakte.clientId is missing"))

lazy val clientSecret = current.configuration.getString("vkontakte.clientSecret").getOrElse(sys.error("vkontakte.clientSecret is missing"))

lazy val callbackUrl = current.configuration.getString("vkontakte.callbackURL").getOrElse(sys.error("vkontakte.callbackURL is missing"))

def retrieveAccessToken(code: String)(implicit ctx: ExecutionContext): Future[AccessToken] = {
WS.url(accessTokenUrl)
.withQueryString(
"client_id" -> clientId,
"client_secret" -> clientSecret,
"redirect_uri" -> callbackUrl,
"code" -> code)
.withHeaders(HeaderNames.ACCEPT -> MimeTypes.JSON)
.post(Results.EmptyContent())
.map { response =>
Logger(getClass).debug("Retrieving access token from provider API: " + response.body)
parseAccessTokenResponse(response)
}
}

def getAuthorizationUrl(scope: String, state: String): String = {
val encodedClientId = URLEncoder.encode(clientId, "utf-8")
val encodedRedirectUri = URLEncoder.encode(callbackUrl, "utf-8")
val encodedScope = URLEncoder.encode(scope, "utf-8")
val encodedState = URLEncoder.encode(state, "utf-8")
s"${authorizationUrl}?client_id=${encodedClientId}" +
s"&redirect_uri=${encodedRedirectUri}" +
s"&display=${display}" +
s"&response_type=${response_type}" +
s"&scope=${encodedScope}" +
s"&state=${encodedState}"
}

def parseAccessTokenResponse(response: WSResponse): String = {
Logger(getClass).debug("Parsing access token response: " + response.body)
try {
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
val responseMap = mapper.readValue[Map[String, Object]](response.body)

var access_token = responseMap.get("access_token")
var email = responseMap.get("email")
var expires_in = responseMap.get("expires_in")
var user_id = responseMap.get("user_id")

VkontakteAuthenticator.email = email.get.toString
VkontakteAuthenticator.expires_in = expires_in.get.toString
VkontakteAuthenticator.user_id = user_id.get.toString

access_token.get.toString
} catch {
case NonFatal(e) =>
throw new AccessTokenRetrievalFailedException(s"Failed to retrieve access token. ${response.body}", e)
}
}

}

object VkontakteAuthenticator {
var email = ""
var expires_in = ""
var user_id = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is global state. it is thread unsafe and very dangerous.

If Vkontakte API returns user info with access token, access token type should be changed from String and VkontakteProviderUserSupport#readProviderUser should extract user info from access token.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. I was thinking about it, but I new in scala so it was the best idea :) I'll fix it


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package jp.t2v.lab.play2.auth.social.providers.vkontakte

/**
* Created by Yuri Rastegaev on 19.03.2016.
*/

import jp.t2v.lab.play2.auth.social.core.OAuth2Controller
import jp.t2v.lab.play2.auth.{AuthConfig, Login, OptionalAuthElement}

trait VkontakteController extends OAuth2Controller
with AuthConfig
with OptionalAuthElement
with Login {

val authenticator = new VkontakteAuthenticator

}
Loading