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

UserService works with trait GenericProfile instead of BasicProfile #477

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
25 changes: 18 additions & 7 deletions README.textile
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
h1. SecureSocial for Play 2
h1. SecSoc for Play 2

SecureSocial allows you to add an authentication UI to your app that works with services based on OAuth1, OAuth2 and OpenID protocols. SecureSocial provides Scala and Java APIs so you can integrate it using your preferred language.
SecSoc allows you to add an authentication to your app that works with services based on OAuth1, OAuth2 and OpenID protocols.
SecSoc provides only Scala APIs.

Check the project web site for more information: <a href="http://www.securesocial.ws">http://www.securesocial.ws</a>
h2. SecureSocial

h1. SecureSocial for Play 1.x
SesSoc is fork from SecureSocial project (<a href="http://www.securesocial.ws">http://www.securesocial.ws</a>).

The old version of SecureSocial is under the 1.x branch now. The 'master' branch is for the Play 2 version only.
SecureSocial written by Jorge Aliss (<a href="http://www.twitter.com/jaliss">@jaliss</a>)

Written by Jorge Aliss (<a href="http://www.twitter.com/jaliss">@jaliss</a>)
h2. Difference from SecureSocial

Main goal of SecSoc is light version of SecureSocial. So SecSoc miss next SecureSocial features:

* Java API. I don't write in Java so can't support it.
* UI support. Authentication UI is good for very quick start. But it also make your project not flexible. For example,
integrate single page authentication and registration is not possible in standard SecureSocial UI.
* Async support. I removed most of Future from function return to plain. I think that it's very overused in SecureSocial
(may be I'm wrong).

Any way, if you need these features you should take original SecureSocial library.

h2. License

SecureSocial is distributed under the "Apache License, Version 2.0":http://www.apache.org/licenses/LICENSE-2.0.html.
SecSoc is distributed under the "Apache License, Version 2.0":http://www.apache.org/licenses/LICENSE-2.0.html.
47 changes: 24 additions & 23 deletions module-code/app/securesocial/controllers/LoginApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package securesocial.controllers

import org.joda.time.DateTime
import securesocial.core._
import play.api.mvc.Action
import play.api.mvc.{RequestHeader, Action, Result}
import scala.concurrent.{ ExecutionContext, Future }
import securesocial.core.SignUpEvent
import securesocial.core.AuthenticationResult.Authenticated
Expand All @@ -36,7 +36,7 @@ class LoginApi(override implicit val env: RuntimeEnvironment[BasicProfile]) exte
*
* @tparam U The application user type
*/
trait BaseLoginApi[U] extends SecureSocial[U] {
trait BaseLoginApi[U <: GenericProfile] extends SecureSocial[U] {

import play.api.libs.json._

Expand All @@ -51,29 +51,15 @@ trait BaseLoginApi[U] extends SecureSocial[U] {
def authenticate(providerId: String, builderId: String) = Action.async { implicit request =>
import ExecutionContext.Implicits.global
val result = for (
builder <- env.authenticatorService.find(builderId);
provider <- env.providers.get(providerId) if provider.isInstanceOf[ApiSupport]
) yield {
provider.asInstanceOf[ApiSupport].authenticateForApi.flatMap {
case authenticated: Authenticated =>
val profile = authenticated.profile
env.userService.find(profile.providerId, profile.userId).flatMap {
maybeExisting =>
val mode = if (maybeExisting.isDefined) SaveMode.LoggedIn else SaveMode.SignUp
env.userService.save(authenticated.profile, mode).flatMap {
userForAction =>
logger.debug(s"[securesocial] user completed authentication: provider = ${profile.providerId}, userId: ${profile.userId}, mode = $mode")
val evt = if (mode == SaveMode.LoggedIn) new LoginEvent(userForAction) else new SignUpEvent(userForAction)
// we're not using a session here .... review this.
Events.fire(evt)
builder.fromUser(userForAction).map { authenticator =>
val token = TokenResponse(authenticator.id, authenticator.expirationDate)
Ok(Json.toJson(token))
}
}
}
Future.successful(processSuccessAuthentication(authenticated, builderId));

case failed: AuthenticationResult.Failed =>
Future.successful(BadRequest(Json.toJson(Map("error" -> failed.error))).as("application/json"))

case other =>
// todo: review this status
logger.error(s"[securesocial] unexpected result from authenticateForApi: $other")
Expand All @@ -83,12 +69,27 @@ trait BaseLoginApi[U] extends SecureSocial[U] {
result.getOrElse(Future.successful(NotFound.as("application/json")))
}

def logout = Action.async { implicit request =>
private def processSuccessAuthentication(authenticated: Authenticated, builderId : String)(implicit requestHeader : RequestHeader) : Result = {
val builder = env.authenticatorService.find(builderId).get
val profile = authenticated.profile
val maybeExisting = env.userService.find(profile.providerId, profile.userId)

val mode = if (maybeExisting.isDefined) SaveMode.LoggedIn else SaveMode.SignUp
val userForAction = env.userService.save(authenticated.profile, mode)
logger.debug(s"[securesocial] user completed authentication: provider = ${profile.providerId}, userId: ${profile.userId}, mode = $mode")
val evt = if (mode == SaveMode.LoggedIn) new LoginEvent(userForAction) else new SignUpEvent(userForAction)
// we're not using a session here .... review this.
Events.fire(evt)
val authenticator = builder.fromUser(userForAction)
val token = TokenResponse(authenticator.id, authenticator.expirationDate)
Ok(Json.toJson(token))
}

def logout = Action { implicit request =>
import securesocial.core.utils._
import ExecutionContext.Implicits.global
env.authenticatorService.fromRequest(request).flatMap {
env.authenticatorService.fromRequest(request) match {
case Some(authenticator) => Ok("").discardingAuthenticator(authenticator)
case None => Future.successful(Ok(""))
case None => Ok("")
}
}
}
Expand Down
12 changes: 5 additions & 7 deletions module-code/app/securesocial/controllers/LoginPage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class LoginPage(override implicit val env: RuntimeEnvironment[BasicProfile]) ext
/**
* The trait that defines the login page controller
*/
trait BaseLoginPage[U] extends SecureSocial[U] {
trait BaseLoginPage[U <: GenericProfile] extends SecureSocial[U] {
private val logger = play.api.Logger("securesocial.controllers.LoginPage")

/**
Expand Down Expand Up @@ -66,20 +66,18 @@ trait BaseLoginPage[U] extends SecureSocial[U] {
*
* @return
*/
def logout = UserAwareAction.async {
def logout = UserAwareAction{
implicit request =>
val redirectTo = Redirect(Play.configuration.getString(onLogoutGoTo).getOrElse(env.routes.loginPageUrl))
val result = for {
user <- request.user
authenticator <- request.authenticator
} yield {
import ExecutionContext.Implicits.global
redirectTo.discardingAuthenticator(authenticator).map {
_.withSession(Events.fire(new LogoutEvent(user)).getOrElse(request.session))
}
val result = redirectTo.discardingAuthenticator(authenticator)
result.withSession(Events.fire(new LogoutEvent(user)).getOrElse(request.session))
}
result.getOrElse {
Future.successful(redirectTo)
redirectTo
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import play.api.data.Form
import play.api.data.Forms._
import play.api.data.validation.Constraints._
import play.api.i18n.Messages
import play.api.mvc.{ RequestHeader, SimpleResult }
import securesocial.core.SecureSocial
import play.api.mvc.{ RequestHeader, Result }
import securesocial.core.{GenericProfile, SecureSocial}
import securesocial.core.providers.MailToken

import scala.concurrent.Future
Expand All @@ -34,7 +34,7 @@ import scala.concurrent.Future
* The base controller for password reset and password change operations
*
*/
abstract class MailTokenBasedOperations[U] extends SecureSocial[U] {
abstract class MailTokenBasedOperations[U <: GenericProfile] extends SecureSocial[U] {
val Success = "success"
val Error = "error"
val Email = "email"
Expand All @@ -53,12 +53,16 @@ abstract class MailTokenBasedOperations[U] extends SecureSocial[U] {
* @param isSignUp a boolean indicating if the token is used for a signup or password reset operation
* @return a MailToken instance
*/
def createToken(email: String, isSignUp: Boolean): Future[MailToken] = {
def createToken(email: String, isSignUp: Boolean): MailToken = {
val now = DateTime.now

Future.successful(MailToken(
UUID.randomUUID().toString, email.toLowerCase, now, now.plusMinutes(TokenDuration), isSignUp = isSignUp
))
MailToken(
UUID.randomUUID().toString,
email.toLowerCase,
now,
now.plusMinutes(TokenDuration),
isSignUp = isSignUp
)
}

/**
Expand All @@ -72,14 +76,14 @@ abstract class MailTokenBasedOperations[U] extends SecureSocial[U] {
* @return the action result
*/
protected def executeForToken(token: String, isSignUp: Boolean,
f: MailToken => Future[SimpleResult])(implicit request: RequestHeader): Future[SimpleResult] =
f: MailToken => Result)(implicit request: RequestHeader): Result =
{
import scala.concurrent.ExecutionContext.Implicits.global
env.userService.findToken(token).flatMap {
env.userService.findToken(token) match {
case Some(t) if !t.isExpired && t.isSignUp == isSignUp => f(t)
case _ =>
val to = if (isSignUp) env.routes.signUpUrl else env.routes.resetPasswordUrl
Future.successful(Redirect(to).flashing(Error -> Messages(BaseRegistration.InvalidLink)))
Redirect(to).flashing(Error -> Messages(BaseRegistration.InvalidLink))
}
}

Expand All @@ -89,13 +93,13 @@ abstract class MailTokenBasedOperations[U] extends SecureSocial[U] {
* @param request the current request
* @return the action result
*/
protected def handleStartResult()(implicit request: RequestHeader): SimpleResult = Redirect(env.routes.loginPageUrl)
protected def handleStartResult()(implicit request: RequestHeader): Result = Redirect(env.routes.loginPageUrl)

/**
* The result sent after the operation has been completed by the user
*
* @param request the current request
* @return the action result
*/
protected def confirmationResult()(implicit request: RequestHeader): SimpleResult = Redirect(env.routes.loginPageUrl)
protected def confirmationResult()(implicit request: RequestHeader): Result = Redirect(env.routes.loginPageUrl)
}
59 changes: 31 additions & 28 deletions module-code/app/securesocial/controllers/PasswordChange.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package securesocial.controllers

import securesocial.core._
import play.api.mvc.SimpleResult
import play.api.mvc.Result
import play.api.Play
import play.api.data.Form
import play.api.data.Forms._
Expand All @@ -27,18 +27,18 @@ import scala.Some
import scala.concurrent.{ Await, ExecutionContext, Future }

/**
* A default PasswordChange controller that uses the BasicProfile as the user type
* A default PasswordChange controller that uses the GenericProfile as the user type
*
* @param env An environment
*/
class PasswordChange(override implicit val env: RuntimeEnvironment[BasicProfile]) extends BasePasswordChange[BasicProfile]
class PasswordChange(override implicit val env: RuntimeEnvironment[GenericProfile]) extends BasePasswordChange[GenericProfile]

/**
* A trait that defines the password change functionality
*
* @tparam U the user object type
*/
trait BasePasswordChange[U] extends SecureSocial[U] {
trait BasePasswordChange[U <: GenericProfile] extends SecureSocial[U] {
val CurrentPassword = "currentPassword"
val InvalidPasswordMessage = "securesocial.passwordChange.invalidPassword"
val NewPassword = "newPassword"
Expand All @@ -65,9 +65,8 @@ trait BasePasswordChange[U] extends SecureSocial[U] {
* @tparam A the type of the user object
* @return a future boolean
*/
def checkCurrentPassword[A](suppliedPassword: String)(implicit request: SecuredRequest[A]): Future[Boolean] = {
import ExecutionContext.Implicits.global
env.userService.passwordInfoFor(request.user).map {
def checkCurrentPassword[A](suppliedPassword: String)(implicit request: SecuredRequest[A]): Boolean = {
env.userService.passwordInfoFor(request.user) match {
case Some(info) =>
env.passwordHashers.get(info.hasher).exists {
_.matches(info, suppliedPassword)
Expand All @@ -76,14 +75,13 @@ trait BasePasswordChange[U] extends SecureSocial[U] {
}
}

private def execute[A](f: Form[ChangeInfo] => Future[SimpleResult])(implicit request: SecuredRequest[A]): Future[SimpleResult] = {
private def execute[A](f: Form[ChangeInfo] => Result) (implicit request: SecuredRequest[A]): Result = {
import ExecutionContext.Implicits.global
val form = Form[ChangeInfo](
mapping(
CurrentPassword ->
nonEmptyText.verifying(Messages(InvalidPasswordMessage), { suppliedPassword =>
import scala.concurrent.duration._
Await.result(checkCurrentPassword(suppliedPassword), 10.seconds)
checkCurrentPassword(suppliedPassword)
}),
NewPassword ->
tuple(
Expand All @@ -94,11 +92,11 @@ trait BasePasswordChange[U] extends SecureSocial[U] {
)((currentPassword, newPassword) => ChangeInfo(currentPassword, newPassword._1))((changeInfo: ChangeInfo) => Some("", ("", "")))
)

env.userService.passwordInfoFor(request.user).flatMap {
env.userService.passwordInfoFor(request.user) match {
case Some(info) =>
f(form)
case None =>
Future.successful(Forbidden)
Forbidden
}
}

Expand All @@ -107,11 +105,9 @@ trait BasePasswordChange[U] extends SecureSocial[U] {
*
* @return
*/
def page = SecuredAction.async { implicit request =>
def page = SecuredAction { implicit request =>
execute { form: Form[ChangeInfo] =>
Future.successful {
Ok(env.viewTemplates.getPasswordChangePage(form))
}
}
}

Expand All @@ -120,26 +116,33 @@ trait BasePasswordChange[U] extends SecureSocial[U] {
*
* @return
*/
def handlePasswordChange = SecuredAction.async { implicit request =>
def handlePasswordChange = SecuredAction{ implicit request =>
execute { form: Form[ChangeInfo] =>
form.bindFromRequest()(request).fold(
errors => Future.successful(BadRequest(env.viewTemplates.getPasswordChangePage(errors))),
errors =>
BadRequest(env.viewTemplates.getPasswordChangePage(errors)),
info => {
val newPasswordInfo = env.currentHasher.hash(info.newPassword)
import ExecutionContext.Implicits.global
implicit val userLang = request2lang(request)
env.userService.updatePasswordInfo(request.user, newPasswordInfo).map {
case Some(u) =>
env.mailer.sendPasswordChangedNotice(u)(request, userLang)
val result = Redirect(onHandlePasswordChangeGoTo).flashing(Success -> Messages(OkMessage)(userLang))
Events.fire(new PasswordChangeEvent(request.user)).map(result.withSession).getOrElse(result)
case None =>
Redirect(onHandlePasswordChangeGoTo).flashing(Error -> Messages("securesocial.password.error")(userLang))
}
changePassword(info)
}
)
}
}

private def changePassword(info: ChangeInfo)(implicit request : SecuredRequest[_]) : Result = {
val newPasswordInfo = env.currentHasher.hash(info.newPassword)

implicit val userLang = request2lang(request)

env.userService.updatePasswordInfo(request.user, newPasswordInfo) match {
case Some(u) =>
env.mailer.sendPasswordChangedNotice(u)(request, userLang)
val result = Redirect(onHandlePasswordChangeGoTo).flashing(Success -> Messages(OkMessage)(userLang))
Events.fire(new PasswordChangeEvent(request.user)).map(result.withSession).getOrElse(result)
case None =>
Redirect(onHandlePasswordChangeGoTo).flashing(Error -> Messages("securesocial.password.error")(userLang))
}

}
}

/**
Expand Down
Loading