├── project ├── build.properties └── plugins.sbt ├── Procfile ├── public ├── images │ ├── favicon.png │ ├── silhouette.png │ └── providers │ │ ├── vk.png │ │ ├── xing.png │ │ ├── google.png │ │ ├── yahoo.png │ │ ├── facebook.png │ │ └── twitter.png └── styles │ └── main.css ├── .gitignore ├── app ├── models │ ├── daos │ │ ├── DAOSlick.scala │ │ ├── UserDAO.scala │ │ ├── UserDAOImpl.scala │ │ ├── OAuth1InfoDAO.scala │ │ ├── PasswordInfoDAO.scala │ │ ├── OAuth2InfoDAO.scala │ │ ├── OpenIDInfoDAO.scala │ │ └── DBTableDefinitions.scala │ ├── User.scala │ └── services │ │ ├── UserService.scala │ │ └── UserServiceImpl.scala ├── utils │ ├── Filters.scala │ └── ErrorHandler.scala ├── forms │ ├── SignInForm.scala │ └── SignUpForm.scala ├── views │ ├── home.scala.html │ ├── signUp.scala.html │ ├── signIn.scala.html │ └── main.scala.html ├── controllers │ ├── ApplicationController.scala │ ├── SocialAuthController.scala │ ├── SignUpController.scala │ └── CredentialsAuthController.scala └── modules │ └── SilhouetteModule.scala ├── activator.properties ├── scripts ├── reformat └── sbt ├── app.json ├── conf ├── routes ├── messages ├── evolutions │ └── default │ │ └── 1.sql ├── application.prod.conf ├── application.conf └── silhouette.conf ├── README.md ├── test └── controllers │ └── ApplicationControllerSpec.scala └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: target/universal/stage/bin/play-silhouette-seed -Dhttp.port=${PORT} -Dconfig.resource=${PLAY_CONF_FILE} 2 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/silhouette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/silhouette.png -------------------------------------------------------------------------------- /public/images/providers/vk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/providers/vk.png -------------------------------------------------------------------------------- /public/images/providers/xing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/providers/xing.png -------------------------------------------------------------------------------- /public/images/providers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/providers/google.png -------------------------------------------------------------------------------- /public/images/providers/yahoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/providers/yahoo.png -------------------------------------------------------------------------------- /public/images/providers/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/providers/facebook.png -------------------------------------------------------------------------------- /public/images/providers/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sbrunk/play-silhouette-slick-seed/HEAD/public/images/providers/twitter.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | /bin 17 | /.cache-main 18 | /.cache-tests 19 | -------------------------------------------------------------------------------- /app/models/daos/DAOSlick.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import slick.driver.JdbcProfile 4 | import play.api.db.slick.HasDatabaseConfigProvider 5 | 6 | /** 7 | * Trait that contains generic slick db handling code to be mixed in with DAOs 8 | */ 9 | trait DAOSlick extends DBTableDefinitions with HasDatabaseConfigProvider[JdbcProfile] -------------------------------------------------------------------------------- /app/utils/Filters.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.Inject 4 | 5 | import play.api.http.HttpFilters 6 | import play.api.mvc.EssentialFilter 7 | import play.filters.csrf.CSRFFilter 8 | import play.filters.headers.SecurityHeadersFilter 9 | 10 | /** 11 | * Provides filters. 12 | */ 13 | class Filters @Inject() (csrfFilter: CSRFFilter, securityHeadersFilter: SecurityHeadersFilter) extends HttpFilters { 14 | override def filters: Seq[EssentialFilter] = Seq(csrfFilter, securityHeadersFilter) 15 | } 16 | -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | # http://typesafe.com/activator/template/contribute 2 | name = play-silhouette-slick-seed 3 | title = The Play Silhouette Slick Seed Project 4 | description = A template for an application which uses Silhouette 3(http://silhouette.mohiva.com/) as authentication library. It uses Slick 3 with asynchronous database I/O and the play-slick plugin to store authentication information in a database. 5 | tags = seed, auth, oauth1, oauth2, openid, credentials, silhouette, authentication, scala, play, slick, database 6 | authorName = Sören Brunk 7 | authorLink = https://github.com/sbrunk 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Warn 3 | 4 | // The Typesafe repository 5 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 6 | 7 | // The Sonatype snapshots repository 8 | resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" 9 | 10 | // Use the Play sbt plugin for Play projects 11 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.2") 12 | 13 | // Use the Scalariform plugin to reformat the code 14 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 15 | -------------------------------------------------------------------------------- /app/forms/SignInForm.scala: -------------------------------------------------------------------------------- 1 | package forms 2 | 3 | import play.api.data.Form 4 | import play.api.data.Forms._ 5 | 6 | /** 7 | * The form which handles the submission of the credentials. 8 | */ 9 | object SignInForm { 10 | 11 | /** 12 | * A play framework form. 13 | */ 14 | val form = Form( 15 | mapping( 16 | "email" -> email, 17 | "password" -> nonEmptyText, 18 | "rememberMe" -> boolean 19 | )(Data.apply)(Data.unapply) 20 | ) 21 | 22 | /** 23 | * The form data. 24 | * 25 | * @param email The email of the user. 26 | * @param password The password of the user. 27 | * @param rememberMe Indicates if the user should stay logged in on the next visit. 28 | */ 29 | case class Data( 30 | email: String, 31 | password: String, 32 | rememberMe: Boolean) 33 | } 34 | -------------------------------------------------------------------------------- /scripts/reformat: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Reformats source code. 4 | # 5 | # Copyright 2015 Mohiva Organisation (license at mohiva dot com) 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | set -o nounset -o errexit 20 | 21 | scripts/sbt scalariformFormat test:scalariformFormat 22 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "play-silhouette-slick-seed", 3 | "description": "Seed project to show how Silhouette can be implemented into a Play Framework application with database access using Slick 3.", 4 | "keywords": [ 5 | "Play", 6 | "Silhouette", 7 | "Slick" 8 | ], 9 | "website": "https://github.com/sbrunk/play-silhouette-slick-seed", 10 | "repository": "https://github.com/sbrunk/play-silhouette-slick-seed", 11 | "success_url": "/", 12 | "env": { 13 | "BUILDPACK_URL": "https://github.com/heroku/heroku-buildpack-scala.git", 14 | "PLAY_CONF_FILE": "application.prod.conf", 15 | "PLAY_APP_SECRET": "changeme", 16 | "FACEBOOK_CLIENT_ID": "", 17 | "FACEBOOK_CLIENT_SECRET": "", 18 | "GOOGLE_CLIENT_ID": "", 19 | "GOOGLE_CLIENT_SECRET": "", 20 | "TWITTER_CONSUMER_KEY": "", 21 | "TWITTER_CONSUMER_SECRET": "" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/forms/SignUpForm.scala: -------------------------------------------------------------------------------- 1 | package forms 2 | 3 | import play.api.data.Form 4 | import play.api.data.Forms._ 5 | 6 | /** 7 | * The form which handles the sign up process. 8 | */ 9 | object SignUpForm { 10 | 11 | /** 12 | * A play framework form. 13 | */ 14 | val form = Form( 15 | mapping( 16 | "firstName" -> nonEmptyText, 17 | "lastName" -> nonEmptyText, 18 | "email" -> email, 19 | "password" -> nonEmptyText 20 | )(Data.apply)(Data.unapply) 21 | ) 22 | 23 | /** 24 | * The form data. 25 | * 26 | * @param firstName The first name of a user. 27 | * @param lastName The last name of a user. 28 | * @param email The email of the user. 29 | * @param password The password of the user. 30 | */ 31 | case class Data( 32 | firstName: String, 33 | lastName: String, 34 | email: String, 35 | password: String) 36 | } 37 | -------------------------------------------------------------------------------- /app/models/User.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import java.util.UUID 4 | 5 | import com.mohiva.play.silhouette.api.{ Identity, LoginInfo } 6 | 7 | /** 8 | * The user object. 9 | * 10 | * @param userID The unique ID of the user. 11 | * @param loginInfo The linked login info. 12 | * @param firstName Maybe the first name of the authenticated user. 13 | * @param lastName Maybe the last name of the authenticated user. 14 | * @param fullName Maybe the full name of the authenticated user. 15 | * @param email Maybe the email of the authenticated provider. 16 | * @param avatarURL Maybe the avatar URL of the authenticated provider. 17 | */ 18 | case class User( 19 | userID: UUID, 20 | loginInfo: LoginInfo, 21 | firstName: Option[String], 22 | lastName: Option[String], 23 | fullName: Option[String], 24 | email: Option[String], 25 | avatarURL: Option[String]) extends Identity 26 | -------------------------------------------------------------------------------- /app/models/services/UserService.scala: -------------------------------------------------------------------------------- 1 | package models.services 2 | 3 | import com.mohiva.play.silhouette.api.services.IdentityService 4 | import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile 5 | import models.User 6 | 7 | import scala.concurrent.Future 8 | 9 | /** 10 | * Handles actions to users. 11 | */ 12 | trait UserService extends IdentityService[User] { 13 | 14 | /** 15 | * Saves a user. 16 | * 17 | * @param user The user to save. 18 | * @return The saved user. 19 | */ 20 | def save(user: User): Future[User] 21 | 22 | /** 23 | * Saves the social profile for a user. 24 | * 25 | * If a user exists for this profile then update the user, otherwise create a new user with the given profile. 26 | * 27 | * @param profile The social profile to save. 28 | * @return The user for whom the profile was saved. 29 | */ 30 | def save(profile: CommonSocialProfile): Future[User] 31 | } 32 | -------------------------------------------------------------------------------- /app/models/daos/UserDAO.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import java.util.UUID 4 | 5 | import com.mohiva.play.silhouette.api.LoginInfo 6 | import models.User 7 | 8 | import scala.concurrent.Future 9 | 10 | /** 11 | * Give access to the user object. 12 | */ 13 | trait UserDAO { 14 | 15 | /** 16 | * Finds a user by its login info. 17 | * 18 | * @param loginInfo The login info of the user to find. 19 | * @return The found user or None if no user for the given login info could be found. 20 | */ 21 | def find(loginInfo: LoginInfo): Future[Option[User]] 22 | 23 | /** 24 | * Finds a user by its user ID. 25 | * 26 | * @param userID The ID of the user to find. 27 | * @return The found user or None if no user for the given ID could be found. 28 | */ 29 | def find(userID: UUID): Future[Option[User]] 30 | 31 | /** 32 | * Saves a user. 33 | * 34 | * @param user The user to save. 35 | * @return The saved user. 36 | */ 37 | def save(user: User): Future[User] 38 | } 39 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.ApplicationController.index 7 | GET /signIn controllers.ApplicationController.signIn 8 | GET /signUp controllers.ApplicationController.signUp 9 | GET /signOut controllers.ApplicationController.signOut 10 | GET /authenticate/:provider controllers.SocialAuthController.authenticate(provider) 11 | POST /authenticate/credentials controllers.CredentialsAuthController.authenticate 12 | POST /signUp controllers.SignUpController.signUp 13 | 14 | # Map static resources from the /public folder to the /assets URL path 15 | GET /assets/*file controllers.Assets.at(path="/public", file) 16 | GET /webjars/*file controllers.WebJarAssets.at(file) 17 | -------------------------------------------------------------------------------- /conf/messages: -------------------------------------------------------------------------------- 1 | error.email = Valid email required 2 | error.required = This field is required 3 | invalid.credentials = Invalid credentials! 4 | access.denied = Access denied! 5 | user.exists = There already exists a user with this email! 6 | could.not.authenticate = Could not authenticate with social provider! Please try again! 7 | 8 | home.title = Silhouette - Home 9 | sign.up.title = Silhouette - Sign Up 10 | sign.in.title = Silhouette - Sign In 11 | 12 | toggle.navigation = Toggle navigation 13 | welcome.signed.in = Welcome, you are now signed in! 14 | 15 | sign.up.account = Sign up for a new account 16 | sign.in.credentials = Sign in with your credentials 17 | 18 | error = Error! 19 | home = Home 20 | first.name = First name 21 | last.name = Last name 22 | full.name = Full name 23 | email = Email 24 | password = Password 25 | sign.up = Sign up 26 | sign.in = Sign in 27 | sign.out = Sign out 28 | sign.in.now = Sign in now 29 | sign.up.now = Sign up now 30 | already.a.member = Already a member? 31 | not.a.member = Not a member? 32 | 33 | remember.me = Remember my login on this computer 34 | or.use.social = Or use your existing account on one of the following services to sign in: 35 | 36 | google = Google 37 | facebook = Facebook 38 | twitter = Twitter 39 | vk = VK 40 | xing = Xing 41 | yahoo = Yahoo 42 | -------------------------------------------------------------------------------- /conf/evolutions/default/1.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | 3 | create table "user" ("userID" VARCHAR NOT NULL PRIMARY KEY,"firstName" VARCHAR,"lastName" VARCHAR,"fullName" VARCHAR,"email" VARCHAR,"avatarURL" VARCHAR); 4 | create table "logininfo" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,"providerID" VARCHAR NOT NULL,"providerKey" VARCHAR NOT NULL); 5 | create table "userlogininfo" ("userID" VARCHAR NOT NULL,"loginInfoId" BIGINT NOT NULL); 6 | create table "passwordinfo" ("hasher" VARCHAR NOT NULL,"password" VARCHAR NOT NULL,"salt" VARCHAR,"loginInfoId" BIGINT NOT NULL); 7 | create table "oauth1info" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,"token" VARCHAR NOT NULL,"secret" VARCHAR NOT NULL,"loginInfoId" BIGINT NOT NULL); 8 | create table "oauth2info" ("id" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) NOT NULL PRIMARY KEY,"accesstoken" VARCHAR NOT NULL,"tokentype" VARCHAR,"expiresin" INTEGER,"refreshtoken" VARCHAR,"logininfoid" BIGINT NOT NULL); 9 | create table "openidinfo" ("id" VARCHAR NOT NULL PRIMARY KEY,"logininfoid" BIGINT NOT NULL); 10 | create table "openidattributes" ("id" VARCHAR NOT NULL,"key" VARCHAR NOT NULL,"value" VARCHAR NOT NULL); 11 | 12 | 13 | # --- !Downs 14 | 15 | drop table "openidattributes"; 16 | drop table "openidinfo"; 17 | drop table "oauth2info"; 18 | drop table "oauth1info"; 19 | drop table "passwordinfo"; 20 | drop table "userlogininfo"; 21 | drop table "logininfo"; 22 | drop table "user"; -------------------------------------------------------------------------------- /conf/application.prod.conf: -------------------------------------------------------------------------------- 1 | include "application.conf" 2 | 3 | play.crypto.secret=${?PLAY_APP_SECRET} 4 | 5 | silhouette { 6 | 7 | # Authenticator settings 8 | authenticator.cookieDomain="play-silhouette-seed.herokuapp.com" 9 | authenticator.secureCookie=true 10 | 11 | # OAuth1 token secret provider settings 12 | oauth1TokenSecretProvider.cookieDomain="play-silhouette-seed.herokuapp.com" 13 | oauth1TokenSecretProvider.secureCookie=true 14 | 15 | # OAuth2 state provider settings 16 | oauth2StateProvider.cookieDomain="play-silhouette-seed.herokuapp.com" 17 | oauth2StateProvider.secureCookie=true 18 | 19 | # Facebook provider 20 | facebook.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/facebook" 21 | 22 | # Google provider 23 | google.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/google" 24 | 25 | # VK provider 26 | vk.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/vk" 27 | 28 | # Clef provider 29 | clef.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/clef" 30 | 31 | # Twitter provider 32 | twitter.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/twitter" 33 | 34 | # Xing provider 35 | xing.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/xing" 36 | 37 | # Yahoo provider 38 | yahoo.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/yahoo" 39 | yahoo.realm="https://play-silhouette-seed.herokuapp.com" 40 | } 41 | -------------------------------------------------------------------------------- /app/views/home.scala.html: -------------------------------------------------------------------------------- 1 | @(user: models.User)(implicit messages: Messages) 2 | 3 | @main(Messages("home.title"), Some(user)) { 4 |
5 |
6 |
7 |

@Messages("welcome.signed.in")

8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |

@Messages("first.name"):

@user.firstName.getOrElse("None")

17 |
18 |
19 |

@Messages("last.name"):

@user.lastName.getOrElse("None")

20 |
21 |
22 |

@Messages("full.name"):

@user.fullName.getOrElse("None")

23 |
24 |
25 |

@Messages("email"):

@user.email.getOrElse("None")

26 |
27 |
28 |
29 |
30 | } 31 | -------------------------------------------------------------------------------- /public/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | h1 { 6 | text-align: center; 7 | font-size: 30px; 8 | } 9 | .starter-template { 10 | padding: 40px 15px; 11 | } 12 | 13 | input, button { 14 | margin: 5px 0; 15 | } 16 | 17 | fieldset { 18 | margin-top: 100px; 19 | } 20 | legend { 21 | font-family: 'Montserrat', sans-serif; 22 | text-align: center; 23 | font-size: 20px; 24 | padding: 15px; 25 | } 26 | a { 27 | cursor: pointer; 28 | } 29 | 30 | .provider { 31 | display: inline-block; 32 | width: 64px; 33 | height: 64px; 34 | border-radius: 4px; 35 | outline: none; 36 | } 37 | .facebook { background: #3B5998; } 38 | .google { background: #D14836; } 39 | .twitter { background: #00ACED; } 40 | .yahoo { background: #731A8B; } 41 | .xing { background: #006567; } 42 | .vk { background: #567ca4; } 43 | 44 | .social-providers, 45 | .sign-in-now, 46 | .already-member, 47 | .not-a-member { 48 | text-align: center; 49 | margin-top: 20px; 50 | } 51 | 52 | .clef-button-wrapper { 53 | width: 190px!important; 54 | height: 34px!important; 55 | margin: 20px auto 0 auto!important; 56 | } 57 | 58 | .user { 59 | margin-top: 50px; 60 | } 61 | .user .data { 62 | margin-top: 10px; 63 | } 64 | 65 | .form-control { 66 | border-radius: 0; 67 | } 68 | 69 | [class^='ion-'] { 70 | font-size: 1.2em; 71 | } 72 | 73 | .has-feedback .form-control-feedback { 74 | top: 0; 75 | left: 0; 76 | width: 46px; 77 | height: 46px; 78 | line-height: 46px; 79 | color: #555; 80 | } 81 | 82 | .has-feedback .form-control { 83 | padding-left: 42.5px; 84 | } 85 | 86 | .btn { 87 | font-weight: bold; 88 | border-radius: 2px; 89 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26); 90 | } 91 | 92 | .btn-lg { 93 | font-size: 18px; 94 | } 95 | -------------------------------------------------------------------------------- /app/views/signUp.scala.html: -------------------------------------------------------------------------------- 1 | @(signInForm: Form[forms.SignUpForm.Data])(implicit request: RequestHeader, messages: Messages) 2 | 3 | @import b3.inline.fieldConstructor 4 | 5 | @main(Messages("sign.up.title")) { 6 | @request.flash.get("error").map { msg => 7 |
8 | × 9 | @Messages("error") @msg 10 |
11 | } 12 |
13 | @Messages("sign.up.account") 14 | @helper.form(action = routes.SignUpController.signUp()) { 15 | @helper.CSRF.formField 16 | @b3.text(signInForm("firstName"), '_hiddenLabel -> Messages("first.name"), 'placeholder -> Messages("first.name"), 'class -> "form-control input-lg") 17 | @b3.text(signInForm("lastName"), '_hiddenLabel -> Messages("last.name"), 'placeholder -> Messages("last.name"), 'class -> "form-control input-lg") 18 | @b3.text(signInForm("email"), '_hiddenLabel -> Messages("email"), 'placeholder -> Messages("email"), 'class -> "form-control input-lg") 19 | @b3.password(signInForm("password"), '_hiddenLabel -> Messages("password"), 'placeholder -> Messages("password"), 'class -> "form-control input-lg") 20 |
21 |
22 | 23 |
24 |
25 |
26 |

@Messages("already.a.member") @Messages("sign.in.now")

27 |
28 | } 29 |
30 | } 31 | -------------------------------------------------------------------------------- /app/utils/ErrorHandler.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.Inject 4 | 5 | import com.mohiva.play.silhouette.api.SecuredErrorHandler 6 | import controllers.routes 7 | import play.api.http.DefaultHttpErrorHandler 8 | import play.api.i18n.Messages 9 | import play.api.mvc.Results._ 10 | import play.api.mvc.{ Result, RequestHeader } 11 | import play.api.routing.Router 12 | import play.api.{ OptionalSourceMapper, Configuration } 13 | 14 | import scala.concurrent.Future 15 | 16 | /** 17 | * A secured error handler. 18 | */ 19 | class ErrorHandler @Inject() ( 20 | env: play.api.Environment, 21 | config: Configuration, 22 | sourceMapper: OptionalSourceMapper, 23 | router: javax.inject.Provider[Router]) 24 | extends DefaultHttpErrorHandler(env, config, sourceMapper, router) 25 | with SecuredErrorHandler { 26 | 27 | /** 28 | * Called when a user is not authenticated. 29 | * 30 | * As defined by RFC 2616, the status code of the response should be 401 Unauthorized. 31 | * 32 | * @param request The request header. 33 | * @param messages The messages for the current language. 34 | * @return The result to send to the client. 35 | */ 36 | override def onNotAuthenticated(request: RequestHeader, messages: Messages): Option[Future[Result]] = { 37 | Some(Future.successful(Redirect(routes.ApplicationController.signIn()))) 38 | } 39 | 40 | /** 41 | * Called when a user is authenticated but not authorized. 42 | * 43 | * As defined by RFC 2616, the status code of the response should be 403 Forbidden. 44 | * 45 | * @param request The request header. 46 | * @param messages The messages for the current language. 47 | * @return The result to send to the client. 48 | */ 49 | override def onNotAuthorized(request: RequestHeader, messages: Messages): Option[Future[Result]] = { 50 | Some(Future.successful(Redirect(routes.ApplicationController.signIn()).flashing("error" -> Messages("access.denied")(messages)))) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/models/services/UserServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package models.services 2 | 3 | import java.util.UUID 4 | import javax.inject.Inject 5 | 6 | import com.mohiva.play.silhouette.api.LoginInfo 7 | import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile 8 | import models.User 9 | import models.daos.UserDAO 10 | import play.api.libs.concurrent.Execution.Implicits._ 11 | 12 | import scala.concurrent.Future 13 | 14 | /** 15 | * Handles actions to users. 16 | * 17 | * @param userDAO The user DAO implementation. 18 | */ 19 | class UserServiceImpl @Inject() (userDAO: UserDAO) extends UserService { 20 | 21 | /** 22 | * Retrieves a user that matches the specified login info. 23 | * 24 | * @param loginInfo The login info to retrieve a user. 25 | * @return The retrieved user or None if no user could be retrieved for the given login info. 26 | */ 27 | def retrieve(loginInfo: LoginInfo): Future[Option[User]] = userDAO.find(loginInfo) 28 | 29 | /** 30 | * Saves a user. 31 | * 32 | * @param user The user to save. 33 | * @return The saved user. 34 | */ 35 | def save(user: User) = userDAO.save(user) 36 | 37 | /** 38 | * Saves the social profile for a user. 39 | * 40 | * If a user exists for this profile then update the user, otherwise create a new user with the given profile. 41 | * 42 | * @param profile The social profile to save. 43 | * @return The user for whom the profile was saved. 44 | */ 45 | def save(profile: CommonSocialProfile) = { 46 | userDAO.find(profile.loginInfo).flatMap { 47 | case Some(user) => // Update user with profile 48 | userDAO.save(user.copy( 49 | firstName = profile.firstName, 50 | lastName = profile.lastName, 51 | fullName = profile.fullName, 52 | email = profile.email, 53 | avatarURL = profile.avatarURL 54 | )) 55 | case None => // Insert a new user 56 | userDAO.save(User( 57 | userID = UUID.randomUUID(), 58 | loginInfo = profile.loginInfo, 59 | firstName = profile.firstName, 60 | lastName = profile.lastName, 61 | fullName = profile.fullName, 62 | email = profile.email, 63 | avatarURL = profile.avatarURL 64 | )) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/controllers/ApplicationController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import com.mohiva.play.silhouette.api.{ Environment, LogoutEvent, Silhouette } 6 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 7 | import com.mohiva.play.silhouette.impl.providers.SocialProviderRegistry 8 | import forms._ 9 | import models.User 10 | import play.api.i18n.MessagesApi 11 | 12 | import scala.concurrent.Future 13 | 14 | /** 15 | * The basic application controller. 16 | * 17 | * @param messagesApi The Play messages API. 18 | * @param env The Silhouette environment. 19 | * @param socialProviderRegistry The social provider registry. 20 | */ 21 | class ApplicationController @Inject() ( 22 | val messagesApi: MessagesApi, 23 | val env: Environment[User, CookieAuthenticator], 24 | socialProviderRegistry: SocialProviderRegistry) 25 | extends Silhouette[User, CookieAuthenticator] { 26 | 27 | /** 28 | * Handles the index action. 29 | * 30 | * @return The result to display. 31 | */ 32 | def index = SecuredAction.async { implicit request => 33 | Future.successful(Ok(views.html.home(request.identity))) 34 | } 35 | 36 | /** 37 | * Handles the Sign In action. 38 | * 39 | * @return The result to display. 40 | */ 41 | def signIn = UserAwareAction.async { implicit request => 42 | request.identity match { 43 | case Some(user) => Future.successful(Redirect(routes.ApplicationController.index())) 44 | case None => Future.successful(Ok(views.html.signIn(SignInForm.form, socialProviderRegistry))) 45 | } 46 | } 47 | 48 | /** 49 | * Handles the Sign Up action. 50 | * 51 | * @return The result to display. 52 | */ 53 | def signUp = UserAwareAction.async { implicit request => 54 | request.identity match { 55 | case Some(user) => Future.successful(Redirect(routes.ApplicationController.index())) 56 | case None => Future.successful(Ok(views.html.signUp(SignUpForm.form))) 57 | } 58 | } 59 | 60 | /** 61 | * Handles the Sign Out action. 62 | * 63 | * @return The result to display. 64 | */ 65 | def signOut = SecuredAction.async { implicit request => 66 | val result = Redirect(routes.ApplicationController.index()) 67 | env.eventBus.publish(LogoutEvent(request.identity, request, request2Messages)) 68 | 69 | env.authenticatorService.discard(request.authenticator, result) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | play.crypto.secret="changeme" 9 | 10 | # The application languages 11 | # ~~~~~ 12 | play.i18n.langs=["en"] 13 | 14 | # Registers the error handler 15 | # ~~~~~ 16 | play.http.errorHandler = "utils.ErrorHandler" 17 | 18 | # Registers the request handler 19 | # ~~~~~ 20 | play.http.requestHandler = "play.api.http.DefaultHttpRequestHandler" 21 | 22 | # Registers the filters 23 | # ~~~~~ 24 | play.http.filters = "utils.Filters" 25 | 26 | # play-slick configuration 27 | # ~~~~~ 28 | slick.dbs.default.driver="slick.driver.H2Driver$" 29 | slick.dbs.default.db.driver="org.h2.Driver" 30 | slick.dbs.default.db.url="jdbc:h2:mem:play" 31 | slick.dbs.default.db.user=sa 32 | slick.dbs.default.db.password="" 33 | 34 | # The application DI modules 35 | # ~~~~~ 36 | play.modules.enabled += "modules.SilhouetteModule" 37 | 38 | # Security Filter Configuration - Content Security Policy 39 | # ~~~~~ 40 | #play.filters.headers.contentSecurityPolicy 41 | # default-src 42 | # 'self' 43 | # img-src 44 | # 'self' 45 | # fbcdn-profile-a.akamaihd.net (Facebook) 46 | # *.twimg.com (Twitter) 47 | # *.googleusercontent.com (Google) 48 | # *.xingassets.com (Xing) 49 | # vk.com (VK) 50 | # *.yimg.com (Yahoo) 51 | # secure.gravatar.com 52 | # style-src 53 | # 'self' 54 | # 'unsafe-inline' 55 | # cdnjs.cloudflare.com 56 | # maxcdn.bootstrapcdn.com 57 | # cdn.jsdelivr.net 58 | # fonts.googleapis.com 59 | # 'unsafe-inline' (in-line css found in bootstrap.min.js) 60 | # font-src 61 | # 'self' 62 | # fonts.gstatic.com 63 | # fonts.googleapis.com 64 | # cdnjs.cloudflare.com 65 | # script-src 66 | # 'self' 67 | # clef.io 68 | # connect-src 69 | # 'self' 70 | # twitter.com 71 | # *.xing.com 72 | # frame-src 73 | # clef.io 74 | play.filters.headers.contentSecurityPolicy="default-src 'self'; img-src 'self' fbcdn-profile-a.akamaihd.net *.twimg.com *.googleusercontent.com *.xingassets.com vk.com *.yimg.com secure.gravatar.com; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com maxcdn.bootstrapcdn.com cdn.jsdelivr.net fonts.googleapis.com; font-src 'self' fonts.gstatic.com fonts.googleapis.com cdnjs.cloudflare.com; script-src 'self' clef.io; connect-src 'self' twitter.com *.xing.com; frame-src clef.io" 75 | 76 | include "silhouette.conf" 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Silhouette Slick Seed Template 2 | ============================== 3 | 4 | This is a fork of the official Silhouette Seed project. If you want to have a first look at Silhouette, I suggest you have a look at the [official project](https://github.com/mohiva/play-silhouette-seed). 5 | 6 | The Silhouette Seed project is an Activator template which shows how [Silhouette](https://github.com/mohiva/play-silhouette) can be implemented in a Play Framework application. It's a starting point which can be extended to fit your needs. 7 | It uses the [play-slick](https://github.com/playframework/play-slick) library for database access. 8 | 9 | ## Example 10 | 11 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 12 | 13 | (The "Build App" phase will take a few minutes) 14 | 15 | Currently, there is no live example of this template. 16 | 17 | ## Features 18 | 19 | * Sign Up 20 | * Sign In (Credentials) 21 | * Social Auth (Facebook, Google+, VK, Twitter, Xing, Yahoo) 22 | * Two-factor authentication with Clef 23 | * Dependency Injection with Guice 24 | * Publishing Events 25 | * Avatar service 26 | * Remember me functionality 27 | * [Security headers](https://www.playframework.com/documentation/2.4.x/SecurityHeaders) 28 | * [CSRF Protection](https://www.playframework.com/documentation/2.4.x/ScalaCsrf) 29 | * play-slick database access 30 | 31 | ## Documentation 32 | 33 | Consulate the [Silhouette documentation](http://silhouette.mohiva.com/docs) for more information. If you need help with the integration of Silhouette into your project, don't hesitate and ask questions in our [mailing list](https://groups.google.com/forum/#!forum/play-silhouette) or on [Stack Overflow](http://stackoverflow.com/questions/tagged/playframework). 34 | 35 | ### Slick 36 | 37 | The template stores all authentication information in a database via [Slick](http://slick.typesafe.com/) It uses an in memory [H2](www.h2database.com/) database by default. 38 | 39 | In order to use another database supported by Slick, you need to change the driver in your application.conf and add the corresponding JDBC driver to your dependencies. The [Play Slick documentation](https://www.playframework.com/documentation/2.4.x/PlaySlick) has more information about database configuration. 40 | 41 | ## Activator 42 | 43 | This project template is also 44 | [hosted at typesafe](https://typesafe.com/activator/template/play-silhouette-slick-seed). 45 | 46 | # License 47 | 48 | The code is licensed under [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). 49 | -------------------------------------------------------------------------------- /app/views/signIn.scala.html: -------------------------------------------------------------------------------- 1 | @(signInForm: Form[forms.SignInForm.Data], socialProviders: com.mohiva.play.silhouette.impl.providers.SocialProviderRegistry)(implicit request: RequestHeader, messages: Messages) 2 | 3 | @import com.mohiva.play.silhouette.impl.providers.oauth2.ClefProvider 4 | @import b3.inline.fieldConstructor 5 | 6 | @main(Messages("sign.in.title")) { 7 | @request.flash.get("error").map { msg => 8 |
9 | × 10 | @Messages("error") @msg 11 |
12 | } 13 |
14 | @Messages("sign.in.credentials") 15 | @helper.form(action = routes.CredentialsAuthController.authenticate()) { 16 | @helper.CSRF.formField 17 | @b3.email(signInForm("email"), '_hiddenLabel -> Messages("email"), 'placeholder -> Messages("email"), 'class -> "form-control input-lg") 18 | @b3.password(signInForm("password"), '_hiddenLabel -> Messages("password"), 'placeholder -> Messages("password"), 'class -> "form-control input-lg") 19 | @b3.checkbox(signInForm("rememberMe"), '_text -> Messages("remember.me"), 'checked -> true) 20 |
21 |
22 | 23 |
24 |
25 | } 26 | 27 |
28 |

@Messages("not.a.member") @Messages("sign.up.now")

29 |
30 | 31 | @if(socialProviders.providers.nonEmpty) { 32 |
33 |

@Messages("or.use.social")

34 |
35 | @for(p <- socialProviders.providers if p.id != "clef") { 36 | @Messages(p.id) 37 | } 38 |
39 | @defining(socialProviders.get[ClefProvider]) { clef => 40 | @if(clef.isDefined) { 41 | 42 | } 43 | } 44 |
45 | } 46 | 47 |
48 | } 49 | -------------------------------------------------------------------------------- /app/controllers/SocialAuthController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import com.mohiva.play.silhouette.api._ 6 | import com.mohiva.play.silhouette.api.exceptions.ProviderException 7 | import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository 8 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 9 | import com.mohiva.play.silhouette.impl.providers._ 10 | import models.User 11 | import models.services.UserService 12 | import play.api.i18n.{ MessagesApi, Messages } 13 | import play.api.libs.concurrent.Execution.Implicits._ 14 | import play.api.mvc.Action 15 | 16 | import scala.concurrent.Future 17 | 18 | /** 19 | * The social auth controller. 20 | * 21 | * @param messagesApi The Play messages API. 22 | * @param env The Silhouette environment. 23 | * @param userService The user service implementation. 24 | * @param authInfoRepository The auth info service implementation. 25 | * @param socialProviderRegistry The social provider registry. 26 | */ 27 | class SocialAuthController @Inject() ( 28 | val messagesApi: MessagesApi, 29 | val env: Environment[User, CookieAuthenticator], 30 | userService: UserService, 31 | authInfoRepository: AuthInfoRepository, 32 | socialProviderRegistry: SocialProviderRegistry) 33 | extends Silhouette[User, CookieAuthenticator] with Logger { 34 | 35 | /** 36 | * Authenticates a user against a social provider. 37 | * 38 | * @param provider The ID of the provider to authenticate against. 39 | * @return The result to display. 40 | */ 41 | def authenticate(provider: String) = Action.async { implicit request => 42 | (socialProviderRegistry.get[SocialProvider](provider) match { 43 | case Some(p: SocialProvider with CommonSocialProfileBuilder) => 44 | p.authenticate().flatMap { 45 | case Left(result) => Future.successful(result) 46 | case Right(authInfo) => for { 47 | profile <- p.retrieveProfile(authInfo) 48 | user <- userService.save(profile) 49 | authInfo <- authInfoRepository.save(profile.loginInfo, authInfo) 50 | authenticator <- env.authenticatorService.create(profile.loginInfo) 51 | value <- env.authenticatorService.init(authenticator) 52 | result <- env.authenticatorService.embed(value, Redirect(routes.ApplicationController.index())) 53 | } yield { 54 | env.eventBus.publish(LoginEvent(user, request, request2Messages)) 55 | result 56 | } 57 | } 58 | case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider")) 59 | }).recover { 60 | case e: ProviderException => 61 | logger.error("Unexpected provider error", e) 62 | Redirect(routes.ApplicationController.signIn()).flashing("error" -> Messages("could.not.authenticate")) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/controllers/ApplicationControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import java.util.UUID 4 | 5 | import com.google.inject.AbstractModule 6 | import com.mohiva.play.silhouette.api.{ Environment, LoginInfo } 7 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 8 | import com.mohiva.play.silhouette.test._ 9 | import models.User 10 | import net.codingwell.scalaguice.ScalaModule 11 | import org.specs2.mock.Mockito 12 | import org.specs2.specification.Scope 13 | import play.api.inject.guice.GuiceApplicationBuilder 14 | import play.api.libs.concurrent.Execution.Implicits._ 15 | import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } 16 | 17 | /** 18 | * Test case for the [[controllers.ApplicationController]] class. 19 | */ 20 | class ApplicationControllerSpec extends PlaySpecification with Mockito { 21 | sequential 22 | 23 | "The `index` action" should { 24 | "redirect to login page if user is unauthorized" in new Context { 25 | new WithApplication(application) { 26 | val Some(redirectResult) = route(FakeRequest(routes.ApplicationController.index()) 27 | .withAuthenticator[CookieAuthenticator](LoginInfo("invalid", "invalid")) 28 | ) 29 | 30 | status(redirectResult) must be equalTo SEE_OTHER 31 | 32 | val redirectURL = redirectLocation(redirectResult).getOrElse("") 33 | redirectURL must contain(routes.ApplicationController.signIn().toString()) 34 | 35 | val Some(unauthorizedResult) = route(FakeRequest(GET, redirectURL)) 36 | 37 | status(unauthorizedResult) must be equalTo OK 38 | contentType(unauthorizedResult) must beSome("text/html") 39 | contentAsString(unauthorizedResult) must contain("Silhouette - Sign In") 40 | } 41 | } 42 | 43 | "return 200 if user is authorized" in new Context { 44 | new WithApplication(application) { 45 | val Some(result) = route(FakeRequest(routes.ApplicationController.index()) 46 | .withAuthenticator[CookieAuthenticator](identity.loginInfo) 47 | ) 48 | 49 | status(result) must beEqualTo(OK) 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * The context. 56 | */ 57 | trait Context extends Scope { 58 | 59 | /** 60 | * A fake Guice module. 61 | */ 62 | class FakeModule extends AbstractModule with ScalaModule { 63 | def configure() = { 64 | bind[Environment[User, CookieAuthenticator]].toInstance(env) 65 | } 66 | } 67 | 68 | /** 69 | * An identity. 70 | */ 71 | val identity = User( 72 | userID = UUID.randomUUID(), 73 | loginInfo = LoginInfo("facebook", "user@facebook.com"), 74 | firstName = None, 75 | lastName = None, 76 | fullName = None, 77 | email = None, 78 | avatarURL = None 79 | ) 80 | 81 | /** 82 | * A Silhouette fake environment. 83 | */ 84 | implicit val env: Environment[User, CookieAuthenticator] = new FakeEnvironment[User, CookieAuthenticator](Seq(identity.loginInfo -> identity)) 85 | 86 | /** 87 | * The application. 88 | */ 89 | lazy val application = new GuiceApplicationBuilder() 90 | .overrides(new FakeModule) 91 | .build() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/controllers/SignUpController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import java.util.UUID 4 | import javax.inject.Inject 5 | 6 | import com.mohiva.play.silhouette.api._ 7 | import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository 8 | import com.mohiva.play.silhouette.api.services.AvatarService 9 | import com.mohiva.play.silhouette.api.util.PasswordHasher 10 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 11 | import com.mohiva.play.silhouette.impl.providers._ 12 | import forms.SignUpForm 13 | import models.User 14 | import models.services.UserService 15 | import play.api.i18n.{ MessagesApi, Messages } 16 | import play.api.libs.concurrent.Execution.Implicits._ 17 | import play.api.mvc.Action 18 | 19 | import scala.concurrent.Future 20 | 21 | /** 22 | * The sign up controller. 23 | * 24 | * @param messagesApi The Play messages API. 25 | * @param env The Silhouette environment. 26 | * @param userService The user service implementation. 27 | * @param authInfoRepository The auth info repository implementation. 28 | * @param avatarService The avatar service implementation. 29 | * @param passwordHasher The password hasher implementation. 30 | */ 31 | class SignUpController @Inject() ( 32 | val messagesApi: MessagesApi, 33 | val env: Environment[User, CookieAuthenticator], 34 | userService: UserService, 35 | authInfoRepository: AuthInfoRepository, 36 | avatarService: AvatarService, 37 | passwordHasher: PasswordHasher) 38 | extends Silhouette[User, CookieAuthenticator] { 39 | 40 | /** 41 | * Registers a new user. 42 | * 43 | * @return The result to display. 44 | */ 45 | def signUp = Action.async { implicit request => 46 | SignUpForm.form.bindFromRequest.fold( 47 | form => Future.successful(BadRequest(views.html.signUp(form))), 48 | data => { 49 | val loginInfo = LoginInfo(CredentialsProvider.ID, data.email) 50 | userService.retrieve(loginInfo).flatMap { 51 | case Some(user) => 52 | Future.successful(Redirect(routes.ApplicationController.signUp()).flashing("error" -> Messages("user.exists"))) 53 | case None => 54 | val authInfo = passwordHasher.hash(data.password) 55 | val user = User( 56 | userID = UUID.randomUUID(), 57 | loginInfo = loginInfo, 58 | firstName = Some(data.firstName), 59 | lastName = Some(data.lastName), 60 | fullName = Some(data.firstName + " " + data.lastName), 61 | email = Some(data.email), 62 | avatarURL = None 63 | ) 64 | for { 65 | avatar <- avatarService.retrieveURL(data.email) 66 | user <- userService.save(user.copy(avatarURL = avatar)) 67 | authInfo <- authInfoRepository.add(loginInfo, authInfo) 68 | authenticator <- env.authenticatorService.create(loginInfo) 69 | value <- env.authenticatorService.init(authenticator) 70 | result <- env.authenticatorService.embed(value, Redirect(routes.ApplicationController.index())) 71 | } yield { 72 | env.eventBus.publish(SignUpEvent(user, request, request2Messages)) 73 | env.eventBus.publish(LoginEvent(user, request, request2Messages)) 74 | result 75 | } 76 | } 77 | } 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, user: Option[models.User] = None)(content: Html)(implicit messages: Messages) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | @title 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 51 |
52 |
53 | @content 54 |
55 |
56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/models/daos/UserDAOImpl.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import java.util.UUID 4 | import com.mohiva.play.silhouette.api.LoginInfo 5 | import models.User 6 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 7 | import slick.dbio.DBIOAction 8 | import javax.inject.Inject 9 | import play.api.db.slick.DatabaseConfigProvider 10 | import scala.concurrent.Future 11 | 12 | /** 13 | * Give access to the user object using Slick 14 | */ 15 | class UserDAOImpl @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends UserDAO with DAOSlick { 16 | 17 | import driver.api._ 18 | 19 | /** 20 | * Finds a user by its login info. 21 | * 22 | * @param loginInfo The login info of the user to find. 23 | * @return The found user or None if no user for the given login info could be found. 24 | */ 25 | def find(loginInfo: LoginInfo) = { 26 | val userQuery = for { 27 | dbLoginInfo <- loginInfoQuery(loginInfo) 28 | dbUserLoginInfo <- slickUserLoginInfos.filter(_.loginInfoId === dbLoginInfo.id) 29 | dbUser <- slickUsers.filter(_.id === dbUserLoginInfo.userID) 30 | } yield dbUser 31 | db.run(userQuery.result.headOption).map { dbUserOption => 32 | dbUserOption.map { user => 33 | User(UUID.fromString(user.userID), loginInfo, user.firstName, user.lastName, user.fullName, user.email, user.avatarURL) 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Finds a user by its user ID. 40 | * 41 | * @param userID The ID of the user to find. 42 | * @return The found user or None if no user for the given ID could be found. 43 | */ 44 | def find(userID: UUID) = { 45 | val query = for { 46 | dbUser <- slickUsers.filter(_.id === userID.toString) 47 | dbUserLoginInfo <- slickUserLoginInfos.filter(_.userID === dbUser.id) 48 | dbLoginInfo <- slickLoginInfos.filter(_.id === dbUserLoginInfo.loginInfoId) 49 | } yield (dbUser, dbLoginInfo) 50 | db.run(query.result.headOption).map { resultOption => 51 | resultOption.map { 52 | case (user, loginInfo) => 53 | User( 54 | UUID.fromString(user.userID), 55 | LoginInfo(loginInfo.providerID, loginInfo.providerKey), 56 | user.firstName, 57 | user.lastName, 58 | user.fullName, 59 | user.email, 60 | user.avatarURL) 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Saves a user. 67 | * 68 | * @param user The user to save. 69 | * @return The saved user. 70 | */ 71 | def save(user: User) = { 72 | val dbUser = DBUser(user.userID.toString, user.firstName, user.lastName, user.fullName, user.email, user.avatarURL) 73 | val dbLoginInfo = DBLoginInfo(None, user.loginInfo.providerID, user.loginInfo.providerKey) 74 | // We don't have the LoginInfo id so we try to get it first. 75 | // If there is no LoginInfo yet for this user we retrieve the id on insertion. 76 | val loginInfoAction = { 77 | val retrieveLoginInfo = slickLoginInfos.filter( 78 | info => info.providerID === user.loginInfo.providerID && 79 | info.providerKey === user.loginInfo.providerKey).result.headOption 80 | val insertLoginInfo = slickLoginInfos.returning(slickLoginInfos.map(_.id)). 81 | into((info, id) => info.copy(id = Some(id))) += dbLoginInfo 82 | for { 83 | loginInfoOption <- retrieveLoginInfo 84 | loginInfo <- loginInfoOption.map(DBIO.successful(_)).getOrElse(insertLoginInfo) 85 | } yield loginInfo 86 | } 87 | // combine database actions to be run sequentially 88 | val actions = (for { 89 | _ <- slickUsers.insertOrUpdate(dbUser) 90 | loginInfo <- loginInfoAction 91 | _ <- slickUserLoginInfos += DBUserLoginInfo(dbUser.userID, loginInfo.id.get) 92 | } yield ()).transactionally 93 | // run actions and return user afterwards 94 | db.run(actions).map(_ => user) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/controllers/CredentialsAuthController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import com.mohiva.play.silhouette.api.Authenticator.Implicits._ 6 | import com.mohiva.play.silhouette.api._ 7 | import com.mohiva.play.silhouette.api.exceptions.ProviderException 8 | import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository 9 | import com.mohiva.play.silhouette.api.util.{ Clock, Credentials } 10 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 11 | import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException 12 | import com.mohiva.play.silhouette.impl.providers._ 13 | import forms.SignInForm 14 | import models.User 15 | import models.services.UserService 16 | import net.ceedubs.ficus.Ficus._ 17 | import play.api.Configuration 18 | import play.api.i18n.{ Messages, MessagesApi } 19 | import play.api.libs.concurrent.Execution.Implicits._ 20 | import play.api.mvc.Action 21 | 22 | import scala.concurrent.Future 23 | import scala.concurrent.duration._ 24 | import scala.language.postfixOps 25 | 26 | /** 27 | * The credentials auth controller. 28 | * 29 | * @param messagesApi The Play messages API. 30 | * @param env The Silhouette environment. 31 | * @param userService The user service implementation. 32 | * @param authInfoRepository The auth info repository implementation. 33 | * @param credentialsProvider The credentials provider. 34 | * @param socialProviderRegistry The social provider registry. 35 | * @param configuration The Play configuration. 36 | * @param clock The clock instance. 37 | */ 38 | class CredentialsAuthController @Inject() ( 39 | val messagesApi: MessagesApi, 40 | val env: Environment[User, CookieAuthenticator], 41 | userService: UserService, 42 | authInfoRepository: AuthInfoRepository, 43 | credentialsProvider: CredentialsProvider, 44 | socialProviderRegistry: SocialProviderRegistry, 45 | configuration: Configuration, 46 | clock: Clock) 47 | extends Silhouette[User, CookieAuthenticator] { 48 | 49 | /** 50 | * Authenticates a user against the credentials provider. 51 | * 52 | * @return The result to display. 53 | */ 54 | def authenticate = Action.async { implicit request => 55 | SignInForm.form.bindFromRequest.fold( 56 | form => Future.successful(BadRequest(views.html.signIn(form, socialProviderRegistry))), 57 | data => { 58 | val credentials = Credentials(data.email, data.password) 59 | credentialsProvider.authenticate(credentials).flatMap { loginInfo => 60 | val result = Redirect(routes.ApplicationController.index()) 61 | userService.retrieve(loginInfo).flatMap { 62 | case Some(user) => 63 | val c = configuration.underlying 64 | env.authenticatorService.create(loginInfo).map { 65 | case authenticator if data.rememberMe => 66 | authenticator.copy( 67 | expirationDateTime = clock.now + c.as[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorExpiry"), 68 | idleTimeout = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorIdleTimeout"), 69 | cookieMaxAge = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.cookieMaxAge") 70 | ) 71 | case authenticator => authenticator 72 | }.flatMap { authenticator => 73 | env.eventBus.publish(LoginEvent(user, request, request2Messages)) 74 | env.authenticatorService.init(authenticator).flatMap { v => 75 | env.authenticatorService.embed(v, result) 76 | } 77 | } 78 | case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) 79 | } 80 | }.recover { 81 | case e: ProviderException => 82 | Redirect(routes.ApplicationController.signIn()).flashing("error" -> Messages("invalid.credentials")) 83 | } 84 | } 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /conf/silhouette.conf: -------------------------------------------------------------------------------- 1 | silhouette { 2 | 3 | # Authenticator settings 4 | authenticator.cookieName="authenticator" 5 | authenticator.cookiePath="/" 6 | authenticator.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set 7 | authenticator.httpOnlyCookie=true 8 | authenticator.useFingerprinting=true 9 | authenticator.authenticatorIdleTimeout=30 minutes 10 | authenticator.authenticatorExpiry=12 hours 11 | 12 | authenticator.rememberMe.cookieMaxAge=30 days 13 | authenticator.rememberMe.authenticatorIdleTimeout=5 days 14 | authenticator.rememberMe.authenticatorExpiry=30 days 15 | 16 | # OAuth1 token secret provider settings 17 | oauth1TokenSecretProvider.cookieName="OAuth1TokenSecret" 18 | oauth1TokenSecretProvider.cookiePath="/" 19 | oauth1TokenSecretProvider.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set 20 | oauth1TokenSecretProvider.httpOnlyCookie=true 21 | oauth1TokenSecretProvider.expirationTime=5 minutes 22 | 23 | # OAuth2 state provider settings 24 | oauth2StateProvider.cookieName="OAuth2State" 25 | oauth2StateProvider.cookiePath="/" 26 | oauth2StateProvider.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set 27 | oauth2StateProvider.httpOnlyCookie=true 28 | oauth2StateProvider.expirationTime=5 minutes 29 | 30 | # Facebook provider 31 | facebook.authorizationURL="https://graph.facebook.com/v2.3/oauth/authorize" 32 | facebook.accessTokenURL="https://graph.facebook.com/v2.3/oauth/access_token" 33 | facebook.redirectURL="http://localhost:9000/authenticate/facebook" 34 | facebook.clientID="" 35 | facebook.clientID=${?FACEBOOK_CLIENT_ID} 36 | facebook.clientSecret="" 37 | facebook.clientSecret=${?FACEBOOK_CLIENT_SECRET} 38 | facebook.scope="email" 39 | 40 | # Google provider 41 | google.authorizationURL="https://accounts.google.com/o/oauth2/auth" 42 | google.accessTokenURL="https://accounts.google.com/o/oauth2/token" 43 | google.redirectURL="http://localhost:9000/authenticate/google" 44 | google.clientID="" 45 | google.clientID=${?GOOGLE_CLIENT_ID} 46 | google.clientSecret="" 47 | google.clientSecret=${?GOOGLE_CLIENT_SECRET} 48 | google.scope="profile email" 49 | 50 | # VK provider 51 | vk.authorizationURL="http://oauth.vk.com/authorize" 52 | vk.accessTokenURL="https://oauth.vk.com/access_token" 53 | vk.redirectURL="http://localhost:9000/authenticate/vk" 54 | vk.clientID="" 55 | vk.clientID=${?VK_CLIENT_ID} 56 | vk.clientSecret="" 57 | vk.clientSecret=${?VK_CLIENT_SECRET} 58 | vk.scope="email" 59 | 60 | # Clef provider 61 | clef.accessTokenURL="https://clef.io/api/v1/authorize" 62 | clef.redirectURL="http://localhost:9000/authenticate/clef" 63 | clef.clientID="" 64 | clef.clientID=${?CLEF_CLIENT_ID} 65 | clef.clientSecret="" 66 | clef.clientSecret=${?CLEF_CLIENT_SECRET} 67 | 68 | # Twitter provider 69 | twitter.requestTokenURL="https://twitter.com/oauth/request_token" 70 | twitter.accessTokenURL="https://twitter.com/oauth/access_token" 71 | twitter.authorizationURL="https://twitter.com/oauth/authenticate" 72 | twitter.callbackURL="http://localhost:9000/authenticate/twitter" 73 | twitter.consumerKey="" 74 | twitter.consumerKey=${?TWITTER_CONSUMER_KEY} 75 | twitter.consumerSecret="" 76 | twitter.consumerSecret=${?TWITTER_CONSUMER_SECRET} 77 | 78 | # Xing provider 79 | xing.requestTokenURL="https://api.xing.com/v1/request_token" 80 | xing.accessTokenURL="https://api.xing.com/v1/access_token" 81 | xing.authorizationURL="https://api.xing.com/v1/authorize" 82 | xing.callbackURL="http://localhost:9000/authenticate/xing" 83 | xing.consumerKey="" 84 | xing.consumerKey=${?XING_CONSUMER_KEY} 85 | xing.consumerSecret="" 86 | xing.consumerSecret=${?XING_CONSUMER_SECRET} 87 | 88 | # Yahoo provider 89 | yahoo.providerURL="https://me.yahoo.com/" 90 | yahoo.callbackURL="http://localhost:9000/authenticate/yahoo" 91 | yahoo.axRequired={ 92 | "fullname": "http://axschema.org/namePerson", 93 | "email": "http://axschema.org/contact/email", 94 | "image": "http://axschema.org/media/image/default" 95 | } 96 | yahoo.realm="http://localhost:9000" 97 | } 98 | -------------------------------------------------------------------------------- /app/models/daos/OAuth1InfoDAO.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | import com.mohiva.play.silhouette.impl.daos.DelegableAuthInfoDAO 5 | import com.mohiva.play.silhouette.impl.providers.OAuth1Info 6 | import javax.inject.Inject 7 | import play.api.libs.concurrent.Execution.Implicits._ 8 | import play.api.db.slick.DatabaseConfigProvider 9 | import scala.concurrent.Future 10 | 11 | /** 12 | * The DAO to store the OAuth1 information. 13 | */ 14 | class OAuth1InfoDAO @Inject() (protected val dbConfigProvider: DatabaseConfigProvider) 15 | extends DelegableAuthInfoDAO[OAuth1Info] with DAOSlick { 16 | 17 | import driver.api._ 18 | 19 | protected def oAuth1InfoQuery(loginInfo: LoginInfo) = for { 20 | dbLoginInfo <- loginInfoQuery(loginInfo) 21 | dbOAuth1Info <- slickOAuth1Infos if dbOAuth1Info.loginInfoId === dbLoginInfo.id 22 | } yield dbOAuth1Info 23 | 24 | // Use subquery workaround instead of join to get authinfo because slick only supports selecting 25 | // from a single table for update/delete queries (https://github.com/slick/slick/issues/684). 26 | protected def oAuth1InfoSubQuery(loginInfo: LoginInfo) = 27 | slickOAuth1Infos.filter(_.loginInfoId in loginInfoQuery(loginInfo).map(_.id)) 28 | 29 | protected def addAction(loginInfo: LoginInfo, authInfo: OAuth1Info) = 30 | loginInfoQuery(loginInfo).result.head.flatMap { dbLoginInfo => 31 | slickOAuth1Infos += DBOAuth1Info(None, authInfo.token, authInfo.secret, dbLoginInfo.id.get) 32 | }.transactionally 33 | 34 | protected def updateAction(loginInfo: LoginInfo, authInfo: OAuth1Info) = 35 | oAuth1InfoSubQuery(loginInfo). 36 | map(dbOAuthInfo => (dbOAuthInfo.token, dbOAuthInfo.secret)). 37 | update((authInfo.token, authInfo.secret)) 38 | 39 | /** 40 | * Finds the auth info which is linked with the specified login info. 41 | * 42 | * @param loginInfo The linked login info. 43 | * @return The retrieved auth info or None if no auth info could be retrieved for the given login info. 44 | */ 45 | def find(loginInfo: LoginInfo): Future[Option[OAuth1Info]] = { 46 | val result = db.run(oAuth1InfoQuery(loginInfo).result.headOption) 47 | result.map { dbOAuth1InfoOption => 48 | dbOAuth1InfoOption.map(dbOAuth1Info => OAuth1Info(dbOAuth1Info.token, dbOAuth1Info.secret)) 49 | } 50 | } 51 | 52 | /** 53 | * Adds new auth info for the given login info. 54 | * 55 | * @param loginInfo The login info for which the auth info should be added. 56 | * @param authInfo The auth info to add. 57 | * @return The added auth info. 58 | */ 59 | def add(loginInfo: LoginInfo, authInfo: OAuth1Info): Future[OAuth1Info] = 60 | db.run(addAction(loginInfo, authInfo)).map(_ => authInfo) 61 | 62 | /** 63 | * Updates the auth info for the given login info. 64 | * 65 | * @param loginInfo The login info for which the auth info should be updated. 66 | * @param authInfo The auth info to update. 67 | * @return The updated auth info. 68 | */ 69 | def update(loginInfo: LoginInfo, authInfo: OAuth1Info): Future[OAuth1Info] = 70 | db.run(updateAction(loginInfo, authInfo)).map(_ => authInfo) 71 | 72 | /** 73 | * Saves the auth info for the given login info. 74 | * 75 | * This method either adds the auth info if it doesn't exists or it updates the auth info 76 | * if it already exists. 77 | * 78 | * @param loginInfo The login info for which the auth info should be saved. 79 | * @param authInfo The auth info to save. 80 | * @return The saved auth info. 81 | */ 82 | def save(loginInfo: LoginInfo, authInfo: OAuth1Info): Future[OAuth1Info] = { 83 | val query = loginInfoQuery(loginInfo).joinLeft(slickOAuth1Infos).on(_.id === _.loginInfoId) 84 | val action = query.result.head.flatMap { 85 | case (dbLoginInfo, Some(dbOAuth1Info)) => updateAction(loginInfo, authInfo) 86 | case (dbLoginInfo, None) => addAction(loginInfo, authInfo) 87 | }.transactionally 88 | db.run(action).map(_ => authInfo) 89 | } 90 | 91 | /** 92 | * Removes the auth info for the given login info. 93 | * 94 | * @param loginInfo The login info for which the auth info should be removed. 95 | * @return A future to wait for the process to be completed. 96 | */ 97 | def remove(loginInfo: LoginInfo): Future[Unit] = 98 | db.run(oAuth1InfoSubQuery(loginInfo).delete).map(_ => ()) 99 | } 100 | -------------------------------------------------------------------------------- /app/models/daos/PasswordInfoDAO.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | import com.mohiva.play.silhouette.api.util.PasswordInfo 5 | import com.mohiva.play.silhouette.impl.daos.DelegableAuthInfoDAO 6 | import play.api.libs.concurrent.Execution.Implicits._ 7 | import javax.inject.Inject 8 | import play.api.libs.concurrent.Execution.Implicits._ 9 | import play.api.db.slick.DatabaseConfigProvider 10 | import scala.concurrent.Future 11 | 12 | /** 13 | * The DAO to store the password information. 14 | */ 15 | class PasswordInfoDAO @Inject() (protected val dbConfigProvider: DatabaseConfigProvider) 16 | extends DelegableAuthInfoDAO[PasswordInfo] with DAOSlick { 17 | 18 | import driver.api._ 19 | 20 | protected def passwordInfoQuery(loginInfo: LoginInfo) = for { 21 | dbLoginInfo <- loginInfoQuery(loginInfo) 22 | dbPasswordInfo <- slickPasswordInfos if dbPasswordInfo.loginInfoId === dbLoginInfo.id 23 | } yield dbPasswordInfo 24 | 25 | // Use subquery workaround instead of join to get authinfo because slick only supports selecting 26 | // from a single table for update/delete queries (https://github.com/slick/slick/issues/684). 27 | protected def passwordInfoSubQuery(loginInfo: LoginInfo) = 28 | slickPasswordInfos.filter(_.loginInfoId in loginInfoQuery(loginInfo).map(_.id)) 29 | 30 | protected def addAction(loginInfo: LoginInfo, authInfo: PasswordInfo) = 31 | loginInfoQuery(loginInfo).result.head.flatMap { dbLoginInfo => 32 | slickPasswordInfos += 33 | DBPasswordInfo(authInfo.hasher, authInfo.password, authInfo.salt, dbLoginInfo.id.get) 34 | }.transactionally 35 | 36 | protected def updateAction(loginInfo: LoginInfo, authInfo: PasswordInfo) = 37 | passwordInfoSubQuery(loginInfo). 38 | map(dbPasswordInfo => (dbPasswordInfo.hasher, dbPasswordInfo.password, dbPasswordInfo.salt)). 39 | update((authInfo.hasher, authInfo.password, authInfo.salt)) 40 | 41 | /** 42 | * Finds the auth info which is linked with the specified login info. 43 | * 44 | * @param loginInfo The linked login info. 45 | * @return The retrieved auth info or None if no auth info could be retrieved for the given login info. 46 | */ 47 | def find(loginInfo: LoginInfo): Future[Option[PasswordInfo]] = { 48 | db.run(passwordInfoQuery(loginInfo).result.headOption).map { dbPasswordInfoOption => 49 | dbPasswordInfoOption.map(dbPasswordInfo => 50 | PasswordInfo(dbPasswordInfo.hasher, dbPasswordInfo.password, dbPasswordInfo.salt)) 51 | } 52 | } 53 | 54 | /** 55 | * Adds new auth info for the given login info. 56 | * 57 | * @param loginInfo The login info for which the auth info should be added. 58 | * @param authInfo The auth info to add. 59 | * @return The added auth info. 60 | */ 61 | def add(loginInfo: LoginInfo, authInfo: PasswordInfo): Future[PasswordInfo] = 62 | db.run(addAction(loginInfo, authInfo)).map(_ => authInfo) 63 | 64 | /** 65 | * Updates the auth info for the given login info. 66 | * 67 | * @param loginInfo The login info for which the auth info should be updated. 68 | * @param authInfo The auth info to update. 69 | * @return The updated auth info. 70 | */ 71 | def update(loginInfo: LoginInfo, authInfo: PasswordInfo): Future[PasswordInfo] = 72 | db.run(updateAction(loginInfo, authInfo)).map(_ => authInfo) 73 | 74 | /** 75 | * Saves the auth info for the given login info. 76 | * 77 | * This method either adds the auth info if it doesn't exists or it updates the auth info 78 | * if it already exists. 79 | * 80 | * @param loginInfo The login info for which the auth info should be saved. 81 | * @param authInfo The auth info to save. 82 | * @return The saved auth info. 83 | */ 84 | def save(loginInfo: LoginInfo, authInfo: PasswordInfo): Future[PasswordInfo] = { 85 | val query = loginInfoQuery(loginInfo).joinLeft(slickPasswordInfos).on(_.id === _.loginInfoId) 86 | val action = query.result.head.flatMap { 87 | case (dbLoginInfo, Some(dbPasswordInfo)) => updateAction(loginInfo, authInfo) 88 | case (dbLoginInfo, None) => addAction(loginInfo, authInfo) 89 | } 90 | db.run(action).map(_ => authInfo) 91 | } 92 | 93 | /** 94 | * Removes the auth info for the given login info. 95 | * 96 | * @param loginInfo The login info for which the auth info should be removed. 97 | * @return A future to wait for the process to be completed. 98 | */ 99 | def remove(loginInfo: LoginInfo): Future[Unit] = 100 | db.run(passwordInfoSubQuery(loginInfo).delete).map(_ => ()) 101 | } 102 | -------------------------------------------------------------------------------- /app/models/daos/OAuth2InfoDAO.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | import com.mohiva.play.silhouette.impl.daos.DelegableAuthInfoDAO 5 | import com.mohiva.play.silhouette.impl.providers.OAuth2Info 6 | import javax.inject.Inject 7 | import play.api.libs.concurrent.Execution.Implicits._ 8 | import play.api.db.slick.DatabaseConfigProvider 9 | import scala.concurrent.Future 10 | 11 | /** 12 | * The DAO to store the OAuth2 information. 13 | */ 14 | class OAuth2InfoDAO @Inject() (protected val dbConfigProvider: DatabaseConfigProvider) 15 | extends DelegableAuthInfoDAO[OAuth2Info] with DAOSlick { 16 | 17 | import driver.api._ 18 | 19 | protected def oAuth2InfoQuery(loginInfo: LoginInfo) = for { 20 | dbLoginInfo <- loginInfoQuery(loginInfo) 21 | dbOAuth2Info <- slickOAuth2Infos if dbOAuth2Info.loginInfoId === dbLoginInfo.id 22 | } yield dbOAuth2Info 23 | 24 | // Use subquery workaround instead of join to get authinfo because slick only supports selecting 25 | // from a single table for update/delete queries (https://github.com/slick/slick/issues/684). 26 | protected def oAuth2InfoSubQuery(loginInfo: LoginInfo) = 27 | slickOAuth2Infos.filter(_.loginInfoId in loginInfoQuery(loginInfo).map(_.id)) 28 | 29 | protected def addAction(loginInfo: LoginInfo, authInfo: OAuth2Info) = 30 | loginInfoQuery(loginInfo).result.head.flatMap { dbLoginInfo => 31 | slickOAuth2Infos += DBOAuth2Info( 32 | None, 33 | authInfo.accessToken, 34 | authInfo.tokenType, 35 | authInfo.expiresIn, 36 | authInfo.refreshToken, 37 | dbLoginInfo.id.get) 38 | }.transactionally 39 | 40 | def updateAction(loginInfo: LoginInfo, authInfo: OAuth2Info) = 41 | oAuth2InfoSubQuery(loginInfo). 42 | map(dbOAuth2Info => (dbOAuth2Info.accessToken, dbOAuth2Info.tokenType, dbOAuth2Info.expiresIn, dbOAuth2Info.refreshToken)). 43 | update((authInfo.accessToken, authInfo.tokenType, authInfo.expiresIn, authInfo.refreshToken)) 44 | 45 | /** 46 | * Finds the auth info which is linked with the specified login info. 47 | * 48 | * @param loginInfo The linked login info. 49 | * @return The retrieved auth info or None if no auth info could be retrieved for the given login info. 50 | */ 51 | def find(loginInfo: LoginInfo): Future[Option[OAuth2Info]] = { 52 | val result = db.run(oAuth2InfoQuery(loginInfo).result.headOption) 53 | result.map { dbOAuth2InfoOption => 54 | dbOAuth2InfoOption.map { dbOAuth2Info => 55 | OAuth2Info(dbOAuth2Info.accessToken, dbOAuth2Info.tokenType, dbOAuth2Info.expiresIn, dbOAuth2Info.refreshToken) 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Adds new auth info for the given login info. 62 | * 63 | * @param loginInfo The login info for which the auth info should be added. 64 | * @param authInfo The auth info to add. 65 | * @return The added auth info. 66 | */ 67 | def add(loginInfo: LoginInfo, authInfo: OAuth2Info): Future[OAuth2Info] = 68 | db.run(addAction(loginInfo, authInfo)).map(_ => authInfo) 69 | 70 | /** 71 | * Updates the auth info for the given login info. 72 | * 73 | * @param loginInfo The login info for which the auth info should be updated. 74 | * @param authInfo The auth info to update. 75 | * @return The updated auth info. 76 | */ 77 | def update(loginInfo: LoginInfo, authInfo: OAuth2Info): Future[OAuth2Info] = 78 | db.run(updateAction(loginInfo, authInfo)).map(_ => authInfo) 79 | 80 | /** 81 | * Saves the auth info for the given login info. 82 | * 83 | * This method either adds the auth info if it doesn't exists or it updates the auth info 84 | * if it already exists. 85 | * 86 | * @param loginInfo The login info for which the auth info should be saved. 87 | * @param authInfo The auth info to save. 88 | * @return The saved auth info. 89 | */ 90 | def save(loginInfo: LoginInfo, authInfo: OAuth2Info): Future[OAuth2Info] = { 91 | val query = for { 92 | result <- loginInfoQuery(loginInfo).joinLeft(slickOAuth2Infos).on(_.id === _.loginInfoId) 93 | } yield result 94 | val action = query.result.head.flatMap { 95 | case (dbLoginInfo, Some(dbOAuth2Info)) => updateAction(loginInfo, authInfo) 96 | case (dbLoginInfo, None) => addAction(loginInfo, authInfo) 97 | }.transactionally 98 | db.run(action).map(_ => authInfo) 99 | } 100 | 101 | /** 102 | * Removes the auth info for the given login info. 103 | * 104 | * @param loginInfo The login info for which the auth info should be removed. 105 | * @return A future to wait for the process to be completed. 106 | */ 107 | def remove(loginInfo: LoginInfo): Future[Unit] = 108 | db.run(oAuth2InfoSubQuery(loginInfo).delete).map(_ => ()) 109 | } 110 | -------------------------------------------------------------------------------- /app/models/daos/OpenIDInfoDAO.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | import com.mohiva.play.silhouette.impl.daos.DelegableAuthInfoDAO 5 | import com.mohiva.play.silhouette.impl.providers.OpenIDInfo 6 | import javax.inject.Inject 7 | import play.api.libs.concurrent.Execution.Implicits._ 8 | import play.api.db.slick.DatabaseConfigProvider 9 | import scala.concurrent.Future 10 | 11 | /** 12 | * The DAO to store the OpenID information. 13 | */ 14 | class OpenIDInfoDAO @Inject() (protected val dbConfigProvider: DatabaseConfigProvider) 15 | extends DelegableAuthInfoDAO[OpenIDInfo] with DAOSlick { 16 | 17 | import driver.api._ 18 | 19 | protected def openIDInfoQuery(loginInfo: LoginInfo) = for { 20 | dbLoginInfo <- loginInfoQuery(loginInfo) 21 | dbOpenIDInfo <- slickOpenIDInfos if dbOpenIDInfo.loginInfoId === dbLoginInfo.id 22 | } yield dbOpenIDInfo 23 | 24 | protected def addAction(loginInfo: LoginInfo, authInfo: OpenIDInfo) = 25 | loginInfoQuery(loginInfo).result.head.flatMap { dbLoginInfo => 26 | DBIO.seq( 27 | slickOpenIDInfos += DBOpenIDInfo(authInfo.id, dbLoginInfo.id.get), 28 | slickOpenIDAttributes ++= authInfo.attributes.map { 29 | case (key, value) => DBOpenIDAttribute(authInfo.id, key, value) 30 | }) 31 | }.transactionally 32 | 33 | protected def updateAction(loginInfo: LoginInfo, authInfo: OpenIDInfo) = 34 | openIDInfoQuery(loginInfo).result.head.flatMap { dbOpenIDInfo => 35 | DBIO.seq( 36 | slickOpenIDInfos filter(_.id === dbOpenIDInfo.id) update dbOpenIDInfo.copy(id = authInfo.id), 37 | slickOpenIDAttributes.filter(_.id === dbOpenIDInfo.id).delete, 38 | slickOpenIDAttributes ++= authInfo.attributes.map { 39 | case (key, value) => DBOpenIDAttribute(authInfo.id, key, value) 40 | }) 41 | }.transactionally 42 | 43 | /** 44 | * Finds the auth info which is linked with the specified login info. 45 | * 46 | * @param loginInfo The linked login info. 47 | * @return The retrieved auth info or None if no auth info could be retrieved for the given login info. 48 | */ 49 | def find(loginInfo: LoginInfo): Future[Option[OpenIDInfo]] = { 50 | val query = openIDInfoQuery(loginInfo).joinLeft(slickOpenIDAttributes).on(_.id === _.id) 51 | val result = db.run(query.result) 52 | result.map { openIDInfos => 53 | if (openIDInfos.isEmpty) None 54 | else { 55 | val attrs = openIDInfos.collect { case (id, Some(attr)) => (attr.key, attr.value) }.toMap 56 | Some(OpenIDInfo(openIDInfos.head._1.id, attrs)) 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Adds new auth info for the given login info. 63 | * 64 | * @param loginInfo The login info for which the auth info should be added. 65 | * @param authInfo The auth info to add. 66 | * @return The added auth info. 67 | */ 68 | def add(loginInfo: LoginInfo, authInfo: OpenIDInfo): Future[OpenIDInfo] = 69 | db.run(addAction(loginInfo, authInfo)).map(_ => authInfo) 70 | 71 | /** 72 | * Updates the auth info for the given login info. 73 | * 74 | * @param loginInfo The login info for which the auth info should be updated. 75 | * @param authInfo The auth info to update. 76 | * @return The updated auth info. 77 | */ 78 | def update(loginInfo: LoginInfo, authInfo: OpenIDInfo): Future[OpenIDInfo] = 79 | db.run(updateAction(loginInfo, authInfo)).map(_ => authInfo) 80 | 81 | /** 82 | * Saves the auth info for the given login info. 83 | * 84 | * This method either adds the auth info if it doesn't exists or it updates the auth info 85 | * if it already exists. 86 | * 87 | * @param loginInfo The login info for which the auth info should be saved. 88 | * @param authInfo The auth info to save. 89 | * @return The saved auth info. 90 | */ 91 | def save(loginInfo: LoginInfo, authInfo: OpenIDInfo): Future[OpenIDInfo] = { 92 | val query = loginInfoQuery(loginInfo).joinLeft(slickOpenIDInfos).on(_.id === _.loginInfoId) 93 | val action = query.result.head.flatMap { 94 | case (dbLoginInfo, Some(dbOpenIDInfo)) => updateAction(loginInfo, authInfo) 95 | case (dbLoginInfo, None) => addAction(loginInfo, authInfo) 96 | } 97 | db.run(action).map(_ => authInfo) 98 | } 99 | 100 | /** 101 | * Removes the auth info for the given login info. 102 | * 103 | * @param loginInfo The login info for which the auth info should be removed. 104 | * @return A future to wait for the process to be completed. 105 | */ 106 | def remove(loginInfo: LoginInfo): Future[Unit] = { 107 | // val attributeQuery = for { 108 | // dbOpenIDInfo <- openIDInfoQuery(loginInfo) 109 | // dbOpenIDAttributes <- slickOpenIDAttributes.filter(_.id === dbOpenIDInfo.id) 110 | //} yield dbOpenIDAttributes 111 | // Use subquery workaround instead of join because slick only supports selecting 112 | // from a single table for update/delete queries (https://github.com/slick/slick/issues/684). 113 | val openIDInfoSubQuery = slickOpenIDInfos.filter(_.loginInfoId in loginInfoQuery(loginInfo).map(_.id)) 114 | val attributeSubQuery = slickOpenIDAttributes.filter(_.id in openIDInfoSubQuery.map(_.id)) 115 | db.run((openIDInfoSubQuery.delete andThen attributeSubQuery.delete).transactionally).map(_ => ()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/models/daos/DBTableDefinitions.scala: -------------------------------------------------------------------------------- 1 | package models.daos 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | import slick.driver.JdbcProfile 5 | import slick.lifted.ProvenShape.proveShapeOf 6 | 7 | trait DBTableDefinitions { 8 | 9 | protected val driver: JdbcProfile 10 | import driver.api._ 11 | 12 | case class DBUser ( 13 | userID: String, 14 | firstName: Option[String], 15 | lastName: Option[String], 16 | fullName: Option[String], 17 | email: Option[String], 18 | avatarURL: Option[String] 19 | ) 20 | 21 | class Users(tag: Tag) extends Table[DBUser](tag, "user") { 22 | def id = column[String]("userID", O.PrimaryKey) 23 | def firstName = column[Option[String]]("firstName") 24 | def lastName = column[Option[String]]("lastName") 25 | def fullName = column[Option[String]]("fullName") 26 | def email = column[Option[String]]("email") 27 | def avatarURL = column[Option[String]]("avatarURL") 28 | def * = (id, firstName, lastName, fullName, email, avatarURL) <> (DBUser.tupled, DBUser.unapply) 29 | } 30 | 31 | case class DBLoginInfo ( 32 | id: Option[Long], 33 | providerID: String, 34 | providerKey: String 35 | ) 36 | 37 | class LoginInfos(tag: Tag) extends Table[DBLoginInfo](tag, "logininfo") { 38 | def id = column[Long]("id", O.PrimaryKey, O.AutoInc) 39 | def providerID = column[String]("providerID") 40 | def providerKey = column[String]("providerKey") 41 | def * = (id.?, providerID, providerKey) <> (DBLoginInfo.tupled, DBLoginInfo.unapply) 42 | } 43 | 44 | case class DBUserLoginInfo ( 45 | userID: String, 46 | loginInfoId: Long 47 | ) 48 | 49 | class UserLoginInfos(tag: Tag) extends Table[DBUserLoginInfo](tag, "userlogininfo") { 50 | def userID = column[String]("userID") 51 | def loginInfoId = column[Long]("loginInfoId") 52 | def * = (userID, loginInfoId) <> (DBUserLoginInfo.tupled, DBUserLoginInfo.unapply) 53 | } 54 | 55 | case class DBPasswordInfo ( 56 | hasher: String, 57 | password: String, 58 | salt: Option[String], 59 | loginInfoId: Long 60 | ) 61 | 62 | class PasswordInfos(tag: Tag) extends Table[DBPasswordInfo](tag, "passwordinfo") { 63 | def hasher = column[String]("hasher") 64 | def password = column[String]("password") 65 | def salt = column[Option[String]]("salt") 66 | def loginInfoId = column[Long]("loginInfoId") 67 | def * = (hasher, password, salt, loginInfoId) <> (DBPasswordInfo.tupled, DBPasswordInfo.unapply) 68 | } 69 | 70 | case class DBOAuth1Info ( 71 | id: Option[Long], 72 | token: String, 73 | secret: String, 74 | loginInfoId: Long 75 | ) 76 | 77 | class OAuth1Infos(tag: Tag) extends Table[DBOAuth1Info](tag, "oauth1info") { 78 | def id = column[Long]("id", O.PrimaryKey, O.AutoInc) 79 | def token = column[String]("token") 80 | def secret = column[String]("secret") 81 | def loginInfoId = column[Long]("loginInfoId") 82 | def * = (id.?, token, secret, loginInfoId) <> (DBOAuth1Info.tupled, DBOAuth1Info.unapply) 83 | } 84 | 85 | case class DBOAuth2Info ( 86 | id: Option[Long], 87 | accessToken: String, 88 | tokenType: Option[String], 89 | expiresIn: Option[Int], 90 | refreshToken: Option[String], 91 | loginInfoId: Long 92 | ) 93 | 94 | class OAuth2Infos(tag: Tag) extends Table[DBOAuth2Info](tag, "oauth2info") { 95 | def id = column[Long]("id", O.PrimaryKey, O.AutoInc) 96 | def accessToken = column[String]("accesstoken") 97 | def tokenType = column[Option[String]]("tokentype") 98 | def expiresIn = column[Option[Int]]("expiresin") 99 | def refreshToken = column[Option[String]]("refreshtoken") 100 | def loginInfoId = column[Long]("logininfoid") 101 | def * = (id.?, accessToken, tokenType, expiresIn, refreshToken, loginInfoId) <> (DBOAuth2Info.tupled, DBOAuth2Info.unapply) 102 | } 103 | 104 | case class DBOpenIDInfo ( 105 | id: String, 106 | loginInfoId: Long 107 | ) 108 | 109 | class OpenIDInfos(tag: Tag) extends Table[DBOpenIDInfo](tag, "openidinfo") { 110 | def id = column[String]("id", O.PrimaryKey) 111 | def loginInfoId = column[Long]("logininfoid") 112 | def * = (id, loginInfoId) <> (DBOpenIDInfo.tupled, DBOpenIDInfo.unapply) 113 | } 114 | 115 | case class DBOpenIDAttribute ( 116 | id: String, 117 | key: String, 118 | value: String 119 | ) 120 | 121 | class OpenIDAttributes(tag: Tag) extends Table[DBOpenIDAttribute](tag, "openidattributes") { 122 | def id = column[String]("id") 123 | def key = column[String]("key") 124 | def value = column[String]("value") 125 | def * = (id, key, value) <> (DBOpenIDAttribute.tupled, DBOpenIDAttribute.unapply) 126 | } 127 | 128 | // table query definitions 129 | val slickUsers = TableQuery[Users] 130 | val slickLoginInfos = TableQuery[LoginInfos] 131 | val slickUserLoginInfos = TableQuery[UserLoginInfos] 132 | val slickPasswordInfos = TableQuery[PasswordInfos] 133 | val slickOAuth1Infos = TableQuery[OAuth1Infos] 134 | val slickOAuth2Infos = TableQuery[OAuth2Infos] 135 | val slickOpenIDInfos = TableQuery[OpenIDInfos] 136 | val slickOpenIDAttributes = TableQuery[OpenIDAttributes] 137 | 138 | // queries used in multiple places 139 | def loginInfoQuery(loginInfo: LoginInfo) = 140 | slickLoginInfos.filter(dbLoginInfo => dbLoginInfo.providerID === loginInfo.providerID && dbLoginInfo.providerKey === loginInfo.providerKey) 141 | } 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /app/modules/SilhouetteModule.scala: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import com.google.inject.{ AbstractModule, Provides } 4 | import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository 5 | import com.mohiva.play.silhouette.api.services._ 6 | import com.mohiva.play.silhouette.api.util._ 7 | import com.mohiva.play.silhouette.api.{ Environment, EventBus } 8 | import com.mohiva.play.silhouette.impl.authenticators._ 9 | import com.mohiva.play.silhouette.impl.daos.DelegableAuthInfoDAO 10 | import com.mohiva.play.silhouette.impl.providers._ 11 | import com.mohiva.play.silhouette.impl.providers.oauth1._ 12 | import com.mohiva.play.silhouette.impl.providers.oauth1.secrets.{ CookieSecretProvider, CookieSecretSettings } 13 | import com.mohiva.play.silhouette.impl.providers.oauth1.services.PlayOAuth1Service 14 | import com.mohiva.play.silhouette.impl.providers.oauth2._ 15 | import com.mohiva.play.silhouette.impl.providers.oauth2.state.{ CookieStateProvider, CookieStateSettings, DummyStateProvider } 16 | import com.mohiva.play.silhouette.impl.providers.openid.YahooProvider 17 | import com.mohiva.play.silhouette.impl.providers.openid.services.PlayOpenIDService 18 | import com.mohiva.play.silhouette.impl.repositories.DelegableAuthInfoRepository 19 | import com.mohiva.play.silhouette.impl.services._ 20 | import com.mohiva.play.silhouette.impl.util._ 21 | import models.User 22 | import models.daos._ 23 | import models.services.{ UserService, UserServiceImpl } 24 | import net.ceedubs.ficus.Ficus._ 25 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 26 | import net.codingwell.scalaguice.ScalaModule 27 | import play.api.Configuration 28 | import play.api.libs.concurrent.Execution.Implicits._ 29 | import play.api.libs.openid.OpenIdClient 30 | import play.api.libs.ws.WSClient 31 | 32 | /** 33 | * The Guice module which wires all Silhouette dependencies. 34 | */ 35 | class SilhouetteModule extends AbstractModule with ScalaModule { 36 | 37 | /** 38 | * Configures the module. 39 | */ 40 | def configure() { 41 | bind[UserService].to[UserServiceImpl] 42 | bind[UserDAO].to[UserDAOImpl] 43 | bind[DelegableAuthInfoDAO[PasswordInfo]].to[PasswordInfoDAO] 44 | bind[DelegableAuthInfoDAO[OAuth1Info]].to[OAuth1InfoDAO] 45 | bind[DelegableAuthInfoDAO[OAuth2Info]].to[OAuth2InfoDAO] 46 | bind[DelegableAuthInfoDAO[OpenIDInfo]].to[OpenIDInfoDAO] 47 | bind[CacheLayer].to[PlayCacheLayer] 48 | bind[IDGenerator].toInstance(new SecureRandomIDGenerator()) 49 | bind[PasswordHasher].toInstance(new BCryptPasswordHasher) 50 | bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false)) 51 | bind[EventBus].toInstance(EventBus()) 52 | bind[Clock].toInstance(Clock()) 53 | } 54 | 55 | /** 56 | * Provides the HTTP layer implementation. 57 | * 58 | * @param client Play's WS client. 59 | * @return The HTTP layer implementation. 60 | */ 61 | @Provides 62 | def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client) 63 | 64 | /** 65 | * Provides the Silhouette environment. 66 | * 67 | * @param userService The user service implementation. 68 | * @param authenticatorService The authentication service implementation. 69 | * @param eventBus The event bus instance. 70 | * @return The Silhouette environment. 71 | */ 72 | @Provides 73 | def provideEnvironment( 74 | userService: UserService, 75 | authenticatorService: AuthenticatorService[CookieAuthenticator], 76 | eventBus: EventBus): Environment[User, CookieAuthenticator] = { 77 | 78 | Environment[User, CookieAuthenticator]( 79 | userService, 80 | authenticatorService, 81 | Seq(), 82 | eventBus 83 | ) 84 | } 85 | 86 | /** 87 | * Provides the social provider registry. 88 | * 89 | * @param facebookProvider The Facebook provider implementation. 90 | * @param googleProvider The Google provider implementation. 91 | * @param vkProvider The VK provider implementation. 92 | * @param clefProvider The Clef provider implementation. 93 | * @param twitterProvider The Twitter provider implementation. 94 | * @param xingProvider The Xing provider implementation. 95 | * @param yahooProvider The Yahoo provider implementation. 96 | * @return The Silhouette environment. 97 | */ 98 | @Provides 99 | def provideSocialProviderRegistry( 100 | facebookProvider: FacebookProvider, 101 | googleProvider: GoogleProvider, 102 | vkProvider: VKProvider, 103 | clefProvider: ClefProvider, 104 | twitterProvider: TwitterProvider, 105 | xingProvider: XingProvider, 106 | yahooProvider: YahooProvider): SocialProviderRegistry = { 107 | 108 | SocialProviderRegistry(Seq( 109 | googleProvider, 110 | facebookProvider, 111 | twitterProvider, 112 | vkProvider, 113 | xingProvider, 114 | yahooProvider, 115 | clefProvider 116 | )) 117 | } 118 | 119 | /** 120 | * Provides the authenticator service. 121 | * 122 | * @param fingerprintGenerator The fingerprint generator implementation. 123 | * @param idGenerator The ID generator implementation. 124 | * @param configuration The Play configuration. 125 | * @param clock The clock instance. 126 | * @return The authenticator service. 127 | */ 128 | @Provides 129 | def provideAuthenticatorService( 130 | fingerprintGenerator: FingerprintGenerator, 131 | idGenerator: IDGenerator, 132 | configuration: Configuration, 133 | clock: Clock): AuthenticatorService[CookieAuthenticator] = { 134 | 135 | val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator") 136 | new CookieAuthenticatorService(config, None, fingerprintGenerator, idGenerator, clock) 137 | } 138 | 139 | /** 140 | * Provides the auth info repository. 141 | * 142 | * @param passwordInfoDAO The implementation of the delegable password auth info DAO. 143 | * @param oauth1InfoDAO The implementation of the delegable OAuth1 auth info DAO. 144 | * @param oauth2InfoDAO The implementation of the delegable OAuth2 auth info DAO. 145 | * @param openIDInfoDAO The implementation of the delegable OpenID auth info DAO. 146 | * @return The auth info repository instance. 147 | */ 148 | @Provides 149 | def provideAuthInfoRepository( 150 | passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo], 151 | oauth1InfoDAO: DelegableAuthInfoDAO[OAuth1Info], 152 | oauth2InfoDAO: DelegableAuthInfoDAO[OAuth2Info], 153 | openIDInfoDAO: DelegableAuthInfoDAO[OpenIDInfo]): AuthInfoRepository = { 154 | 155 | new DelegableAuthInfoRepository(passwordInfoDAO, oauth1InfoDAO, oauth2InfoDAO, openIDInfoDAO) 156 | } 157 | 158 | /** 159 | * Provides the avatar service. 160 | * 161 | * @param httpLayer The HTTP layer implementation. 162 | * @return The avatar service implementation. 163 | */ 164 | @Provides 165 | def provideAvatarService(httpLayer: HTTPLayer): AvatarService = new GravatarService(httpLayer) 166 | 167 | /** 168 | * Provides the OAuth1 token secret provider. 169 | * 170 | * @param configuration The Play configuration. 171 | * @param clock The clock instance. 172 | * @return The OAuth1 token secret provider implementation. 173 | */ 174 | @Provides 175 | def provideOAuth1TokenSecretProvider(configuration: Configuration, clock: Clock): OAuth1TokenSecretProvider = { 176 | val settings = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider") 177 | new CookieSecretProvider(settings, clock) 178 | } 179 | 180 | /** 181 | * Provides the OAuth2 state provider. 182 | * 183 | * @param idGenerator The ID generator implementation. 184 | * @param configuration The Play configuration. 185 | * @param clock The clock instance. 186 | * @return The OAuth2 state provider implementation. 187 | */ 188 | @Provides 189 | def provideOAuth2StateProvider(idGenerator: IDGenerator, configuration: Configuration, clock: Clock): OAuth2StateProvider = { 190 | val settings = configuration.underlying.as[CookieStateSettings]("silhouette.oauth2StateProvider") 191 | new CookieStateProvider(settings, idGenerator, clock) 192 | } 193 | 194 | /** 195 | * Provides the credentials provider. 196 | * 197 | * @param authInfoRepository The auth info repository implementation. 198 | * @param passwordHasher The default password hasher implementation. 199 | * @return The credentials provider. 200 | */ 201 | @Provides 202 | def provideCredentialsProvider( 203 | authInfoRepository: AuthInfoRepository, 204 | passwordHasher: PasswordHasher): CredentialsProvider = { 205 | 206 | new CredentialsProvider(authInfoRepository, passwordHasher, Seq(passwordHasher)) 207 | } 208 | 209 | /** 210 | * Provides the Facebook provider. 211 | * 212 | * @param httpLayer The HTTP layer implementation. 213 | * @param stateProvider The OAuth2 state provider implementation. 214 | * @param configuration The Play configuration. 215 | * @return The Facebook provider. 216 | */ 217 | @Provides 218 | def provideFacebookProvider( 219 | httpLayer: HTTPLayer, 220 | stateProvider: OAuth2StateProvider, 221 | configuration: Configuration): FacebookProvider = { 222 | 223 | new FacebookProvider(httpLayer, stateProvider, configuration.underlying.as[OAuth2Settings]("silhouette.facebook")) 224 | } 225 | 226 | /** 227 | * Provides the Google provider. 228 | * 229 | * @param httpLayer The HTTP layer implementation. 230 | * @param stateProvider The OAuth2 state provider implementation. 231 | * @param configuration The Play configuration. 232 | * @return The Google provider. 233 | */ 234 | @Provides 235 | def provideGoogleProvider( 236 | httpLayer: HTTPLayer, 237 | stateProvider: OAuth2StateProvider, 238 | configuration: Configuration): GoogleProvider = { 239 | 240 | new GoogleProvider(httpLayer, stateProvider, configuration.underlying.as[OAuth2Settings]("silhouette.google")) 241 | } 242 | 243 | /** 244 | * Provides the VK provider. 245 | * 246 | * @param httpLayer The HTTP layer implementation. 247 | * @param stateProvider The OAuth2 state provider implementation. 248 | * @param configuration The Play configuration. 249 | * @return The VK provider. 250 | */ 251 | @Provides 252 | def provideVKProvider( 253 | httpLayer: HTTPLayer, 254 | stateProvider: OAuth2StateProvider, 255 | configuration: Configuration): VKProvider = { 256 | 257 | new VKProvider(httpLayer, stateProvider, configuration.underlying.as[OAuth2Settings]("silhouette.vk")) 258 | } 259 | 260 | /** 261 | * Provides the Clef provider. 262 | * 263 | * @param httpLayer The HTTP layer implementation. 264 | * @param configuration The Play configuration. 265 | * @return The Clef provider. 266 | */ 267 | @Provides 268 | def provideClefProvider(httpLayer: HTTPLayer, configuration: Configuration): ClefProvider = { 269 | 270 | new ClefProvider(httpLayer, new DummyStateProvider, configuration.underlying.as[OAuth2Settings]("silhouette.clef")) 271 | } 272 | 273 | /** 274 | * Provides the Twitter provider. 275 | * 276 | * @param httpLayer The HTTP layer implementation. 277 | * @param tokenSecretProvider The token secret provider implementation. 278 | * @param configuration The Play configuration. 279 | * @return The Twitter provider. 280 | */ 281 | @Provides 282 | def provideTwitterProvider( 283 | httpLayer: HTTPLayer, 284 | tokenSecretProvider: OAuth1TokenSecretProvider, 285 | configuration: Configuration): TwitterProvider = { 286 | 287 | val settings = configuration.underlying.as[OAuth1Settings]("silhouette.twitter") 288 | new TwitterProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) 289 | } 290 | 291 | /** 292 | * Provides the Xing provider. 293 | * 294 | * @param httpLayer The HTTP layer implementation. 295 | * @param tokenSecretProvider The token secret provider implementation. 296 | * @param configuration The Play configuration. 297 | * @return The Xing provider. 298 | */ 299 | @Provides 300 | def provideXingProvider( 301 | httpLayer: HTTPLayer, 302 | tokenSecretProvider: OAuth1TokenSecretProvider, 303 | configuration: Configuration): XingProvider = { 304 | 305 | val settings = configuration.underlying.as[OAuth1Settings]("silhouette.xing") 306 | new XingProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) 307 | } 308 | 309 | /** 310 | * Provides the Yahoo provider. 311 | * 312 | * @param cacheLayer The cache layer implementation. 313 | * @param httpLayer The HTTP layer implementation. 314 | * @param client The OpenID client implementation. 315 | * @param configuration The Play configuration. 316 | * @return The Yahoo provider. 317 | */ 318 | @Provides 319 | def provideYahooProvider( 320 | cacheLayer: CacheLayer, 321 | httpLayer: HTTPLayer, 322 | client: OpenIdClient, 323 | configuration: Configuration): YahooProvider = { 324 | 325 | val settings = configuration.underlying.as[OpenIDSettings]("silhouette.yahoo") 326 | new YahooProvider(httpLayer, new PlayOpenIDService(client, settings), settings) 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /scripts/sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A more capable sbt runner, coincidentally also called sbt. 4 | # Author: Paul Phillips 5 | 6 | # todo - make this dynamic 7 | declare -r sbt_release_version="0.13.8" 8 | declare -r sbt_unreleased_version="0.13.8" 9 | declare -r buildProps="project/build.properties" 10 | 11 | declare sbt_jar sbt_dir sbt_create sbt_version 12 | declare scala_version sbt_explicit_version 13 | declare verbose noshare batch trace_level log_level 14 | declare sbt_saved_stty debugUs 15 | 16 | echoerr () { echo >&2 "$@"; } 17 | vlog () { [[ -n "$verbose" ]] && echoerr "$@"; } 18 | 19 | # spaces are possible, e.g. sbt.version = 0.13.0 20 | build_props_sbt () { 21 | [[ -r "$buildProps" ]] && \ 22 | grep '^sbt\.version' "$buildProps" | tr '=\r' ' ' | awk '{ print $2; }' 23 | } 24 | 25 | update_build_props_sbt () { 26 | local ver="$1" 27 | local old="$(build_props_sbt)" 28 | 29 | [[ -r "$buildProps" ]] && [[ "$ver" != "$old" ]] && { 30 | perl -pi -e "s/^sbt\.version\b.*\$/sbt.version=${ver}/" "$buildProps" 31 | grep -q '^sbt.version[ =]' "$buildProps" || printf "\nsbt.version=%s\n" "$ver" >> "$buildProps" 32 | 33 | vlog "!!!" 34 | vlog "!!! Updated file $buildProps setting sbt.version to: $ver" 35 | vlog "!!! Previous value was: $old" 36 | vlog "!!!" 37 | } 38 | } 39 | 40 | set_sbt_version () { 41 | sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" 42 | [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version 43 | export sbt_version 44 | } 45 | 46 | # restore stty settings (echo in particular) 47 | onSbtRunnerExit() { 48 | [[ -n "$sbt_saved_stty" ]] || return 49 | vlog "" 50 | vlog "restoring stty: $sbt_saved_stty" 51 | stty "$sbt_saved_stty" 52 | unset sbt_saved_stty 53 | } 54 | 55 | # save stty and trap exit, to ensure echo is reenabled if we are interrupted. 56 | trap onSbtRunnerExit EXIT 57 | sbt_saved_stty="$(stty -g 2>/dev/null)" 58 | vlog "Saved stty: $sbt_saved_stty" 59 | 60 | # this seems to cover the bases on OSX, and someone will 61 | # have to tell me about the others. 62 | get_script_path () { 63 | local path="$1" 64 | [[ -L "$path" ]] || { echo "$path" ; return; } 65 | 66 | local target="$(readlink "$path")" 67 | if [[ "${target:0:1}" == "/" ]]; then 68 | echo "$target" 69 | else 70 | echo "${path%/*}/$target" 71 | fi 72 | } 73 | 74 | die() { 75 | echo "Aborting: $@" 76 | exit 1 77 | } 78 | 79 | make_url () { 80 | version="$1" 81 | 82 | case "$version" in 83 | 0.7.*) echo "http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.7.jar" ;; 84 | 0.10.* ) echo "$sbt_launch_repo/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 85 | 0.11.[12]) echo "$sbt_launch_repo/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 86 | *) echo "$sbt_launch_repo/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; 87 | esac 88 | } 89 | 90 | init_default_option_file () { 91 | local overriding_var="${!1}" 92 | local default_file="$2" 93 | if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then 94 | local envvar_file="${BASH_REMATCH[1]}" 95 | if [[ -r "$envvar_file" ]]; then 96 | default_file="$envvar_file" 97 | fi 98 | fi 99 | echo "$default_file" 100 | } 101 | 102 | declare -r cms_opts="-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC" 103 | declare -r jit_opts="-XX:ReservedCodeCacheSize=256m -XX:+TieredCompilation" 104 | declare -r default_jvm_opts_common="-Xms512m -Xmx1536m -Xss2m $jit_opts $cms_opts" 105 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" 106 | declare -r latest_28="2.8.2" 107 | declare -r latest_29="2.9.3" 108 | declare -r latest_210="2.10.5" 109 | declare -r latest_211="2.11.6" 110 | 111 | declare -r script_path="$(get_script_path "$BASH_SOURCE")" 112 | declare -r script_name="${script_path##*/}" 113 | 114 | # some non-read-onlies set with defaults 115 | declare java_cmd="java" 116 | declare sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" 117 | declare jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" 118 | declare sbt_launch_repo="http://typesafe.artifactoryonline.com/typesafe/ivy-releases" 119 | 120 | # pull -J and -D options to give to java. 121 | declare -a residual_args 122 | declare -a java_args 123 | declare -a scalac_args 124 | declare -a sbt_commands 125 | 126 | # args to jvm/sbt via files or environment variables 127 | declare -a extra_jvm_opts extra_sbt_opts 128 | 129 | addJava () { 130 | vlog "[addJava] arg = '$1'" 131 | java_args=( "${java_args[@]}" "$1" ) 132 | } 133 | addSbt () { 134 | vlog "[addSbt] arg = '$1'" 135 | sbt_commands=( "${sbt_commands[@]}" "$1" ) 136 | } 137 | setThisBuild () { 138 | vlog "[addBuild] args = '$@'" 139 | local key="$1" && shift 140 | addSbt "set $key in ThisBuild := $@" 141 | } 142 | addScalac () { 143 | vlog "[addScalac] arg = '$1'" 144 | scalac_args=( "${scalac_args[@]}" "$1" ) 145 | } 146 | addResidual () { 147 | vlog "[residual] arg = '$1'" 148 | residual_args=( "${residual_args[@]}" "$1" ) 149 | } 150 | addResolver () { 151 | addSbt "set resolvers += $1" 152 | } 153 | addDebugger () { 154 | addJava "-Xdebug" 155 | addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" 156 | } 157 | setScalaVersion () { 158 | [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' 159 | addSbt "++ $1" 160 | } 161 | setJavaHome () { 162 | java_cmd="$1/bin/java" 163 | setThisBuild javaHome "Some(file(\"$1\"))" 164 | export JAVA_HOME="$1" 165 | export JDK_HOME="$1" 166 | export PATH="$JAVA_HOME/bin:$PATH" 167 | } 168 | setJavaHomeQuietly () { 169 | java_cmd="$1/bin/java" 170 | addSbt ";warn ;set javaHome in ThisBuild := Some(file(\"$1\")) ;info" 171 | export JAVA_HOME="$1" 172 | export JDK_HOME="$1" 173 | export PATH="$JAVA_HOME/bin:$PATH" 174 | } 175 | 176 | # if set, use JDK_HOME/JAVA_HOME over java found in path 177 | if [[ -e "$JDK_HOME/lib/tools.jar" ]]; then 178 | setJavaHomeQuietly "$JDK_HOME" 179 | elif [[ -e "$JAVA_HOME/bin/java" ]]; then 180 | setJavaHomeQuietly "$JAVA_HOME" 181 | fi 182 | 183 | # directory to store sbt launchers 184 | declare sbt_launch_dir="$HOME/.sbt/launchers" 185 | [[ -d "$sbt_launch_dir" ]] || mkdir -p "$sbt_launch_dir" 186 | [[ -w "$sbt_launch_dir" ]] || sbt_launch_dir="$(mktemp -d -t sbt_extras_launchers.XXXXXX)" 187 | 188 | java_version () { 189 | local version=$("$java_cmd" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d \") 190 | vlog "Detected Java version: $version" 191 | echo "${version:2:1}" 192 | } 193 | 194 | # MaxPermSize critical on pre-8 jvms but incurs noisy warning on 8+ 195 | default_jvm_opts () { 196 | local v="$(java_version)" 197 | if [[ $v -ge 8 ]]; then 198 | echo "$default_jvm_opts_common" 199 | else 200 | echo "-XX:MaxPermSize=384m $default_jvm_opts_common" 201 | fi 202 | } 203 | 204 | build_props_scala () { 205 | if [[ -r "$buildProps" ]]; then 206 | versionLine="$(grep '^build.scala.versions' "$buildProps")" 207 | versionString="${versionLine##build.scala.versions=}" 208 | echo "${versionString%% .*}" 209 | fi 210 | } 211 | 212 | execRunner () { 213 | # print the arguments one to a line, quoting any containing spaces 214 | vlog "# Executing command line:" && { 215 | for arg; do 216 | if [[ -n "$arg" ]]; then 217 | if printf "%s\n" "$arg" | grep -q ' '; then 218 | printf >&2 "\"%s\"\n" "$arg" 219 | else 220 | printf >&2 "%s\n" "$arg" 221 | fi 222 | fi 223 | done 224 | vlog "" 225 | } 226 | 227 | [[ -n "$batch" ]] && exec /dev/null; then 249 | curl --fail --silent "$url" --output "$jar" 250 | elif which wget >/dev/null; then 251 | wget --quiet -O "$jar" "$url" 252 | fi 253 | } && [[ -r "$jar" ]] 254 | } 255 | 256 | acquire_sbt_jar () { 257 | sbt_url="$(jar_url "$sbt_version")" 258 | sbt_jar="$(jar_file "$sbt_version")" 259 | 260 | [[ -r "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar" 261 | } 262 | 263 | usage () { 264 | cat < display stack traces with a max of frames (default: -1, traces suppressed) 283 | -debug-inc enable debugging log for the incremental compiler 284 | -no-colors disable ANSI color codes 285 | -sbt-create start sbt even if current directory contains no sbt project 286 | -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) 287 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) 288 | -ivy path to local Ivy repository (default: ~/.ivy2) 289 | -no-share use all local caches; no sharing 290 | -offline put sbt in offline mode 291 | -jvm-debug Turn on JVM debugging, open at the given port. 292 | -batch Disable interactive mode 293 | -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted 294 | 295 | # sbt version (default: sbt.version from $buildProps if present, otherwise $sbt_release_version) 296 | -sbt-force-latest force the use of the latest release of sbt: $sbt_release_version 297 | -sbt-version use the specified version of sbt (default: $sbt_release_version) 298 | -sbt-dev use the latest pre-release version of sbt: $sbt_unreleased_version 299 | -sbt-jar use the specified jar as the sbt launcher 300 | -sbt-launch-dir directory to hold sbt launchers (default: ~/.sbt/launchers) 301 | -sbt-launch-repo repo url for downloading sbt launcher jar (default: $sbt_launch_repo) 302 | 303 | # scala version (default: as chosen by sbt) 304 | -28 use $latest_28 305 | -29 use $latest_29 306 | -210 use $latest_210 307 | -211 use $latest_211 308 | -scala-home use the scala build at the specified directory 309 | -scala-version use the specified version of scala 310 | -binary-version use the specified scala version when searching for dependencies 311 | 312 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 313 | -java-home alternate JAVA_HOME 314 | 315 | # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution 316 | # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found 317 | $(default_jvm_opts) 318 | JVM_OPTS environment variable holding either the jvm args directly, or 319 | the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') 320 | Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. 321 | -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) 322 | -Dkey=val pass -Dkey=val directly to the jvm 323 | -J-X pass option -X directly to the jvm (-J is stripped) 324 | 325 | # passing options to sbt, OR to this runner 326 | SBT_OPTS environment variable holding either the sbt args directly, or 327 | the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') 328 | Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. 329 | -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) 330 | -S-X add -X to sbt's scalacOptions (-S is stripped) 331 | EOM 332 | } 333 | 334 | process_args () 335 | { 336 | require_arg () { 337 | local type="$1" 338 | local opt="$2" 339 | local arg="$3" 340 | 341 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 342 | die "$opt requires <$type> argument" 343 | fi 344 | } 345 | while [[ $# -gt 0 ]]; do 346 | case "$1" in 347 | -h|-help) usage; exit 1 ;; 348 | -v) verbose=true && shift ;; 349 | -d) addSbt "--debug" && addSbt debug && shift ;; 350 | -w) addSbt "--warn" && addSbt warn && shift ;; 351 | -q) addSbt "--error" && addSbt error && shift ;; 352 | -x) debugUs=true && shift ;; 353 | -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; 354 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; 355 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; 356 | -no-share) noshare=true && shift ;; 357 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; 358 | -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; 359 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; 360 | -offline) addSbt "set offline := true" && shift ;; 361 | -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; 362 | -batch) batch=true && shift ;; 363 | -prompt) require_arg "expr" "$1" "$2" && setThisBuild shellPrompt "(s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; 364 | 365 | -sbt-create) sbt_create=true && shift ;; 366 | -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; 367 | -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; 368 | -sbt-force-latest) sbt_explicit_version="$sbt_release_version" && shift ;; 369 | -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; 370 | -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; 371 | -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; 372 | -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; 373 | -binary-version) require_arg version "$1" "$2" && setThisBuild scalaBinaryVersion "\"$2\"" && shift 2 ;; 374 | -scala-home) require_arg path "$1" "$2" && setThisBuild scalaHome "Some(file(\"$2\"))" && shift 2 ;; 375 | -java-home) require_arg path "$1" "$2" && setJavaHome "$2" && shift 2 ;; 376 | -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; 377 | -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; 378 | 379 | -D*) addJava "$1" && shift ;; 380 | -J*) addJava "${1:2}" && shift ;; 381 | -S*) addScalac "${1:2}" && shift ;; 382 | -28) setScalaVersion "$latest_28" && shift ;; 383 | -29) setScalaVersion "$latest_29" && shift ;; 384 | -210) setScalaVersion "$latest_210" && shift ;; 385 | -211) setScalaVersion "$latest_211" && shift ;; 386 | 387 | --debug) addSbt debug && addResidual "$1" && shift ;; 388 | --warn) addSbt warn && addResidual "$1" && shift ;; 389 | --error) addSbt error && addResidual "$1" && shift ;; 390 | *) addResidual "$1" && shift ;; 391 | esac 392 | done 393 | } 394 | 395 | # process the direct command line arguments 396 | process_args "$@" 397 | 398 | # skip #-styled comments and blank lines 399 | readConfigFile() { 400 | while read line; do 401 | [[ $line =~ ^# ]] || [[ -z $line ]] || echo "$line" 402 | done < "$1" 403 | } 404 | 405 | # if there are file/environment sbt_opts, process again so we 406 | # can supply args to this runner 407 | if [[ -r "$sbt_opts_file" ]]; then 408 | vlog "Using sbt options defined in file $sbt_opts_file" 409 | while read opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") 410 | elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then 411 | vlog "Using sbt options defined in variable \$SBT_OPTS" 412 | extra_sbt_opts=( $SBT_OPTS ) 413 | else 414 | vlog "No extra sbt options have been defined" 415 | fi 416 | 417 | [[ -n "${extra_sbt_opts[*]}" ]] && process_args "${extra_sbt_opts[@]}" 418 | 419 | # reset "$@" to the residual args 420 | set -- "${residual_args[@]}" 421 | argumentCount=$# 422 | 423 | # set sbt version 424 | set_sbt_version 425 | 426 | # only exists in 0.12+ 427 | setTraceLevel() { 428 | case "$sbt_version" in 429 | "0.7."* | "0.10."* | "0.11."* ) echoerr "Cannot set trace level in sbt version $sbt_version" ;; 430 | *) setThisBuild traceLevel $trace_level ;; 431 | esac 432 | } 433 | 434 | # set scalacOptions if we were given any -S opts 435 | [[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\"" 436 | 437 | # Update build.properties on disk to set explicit version - sbt gives us no choice 438 | [[ -n "$sbt_explicit_version" ]] && update_build_props_sbt "$sbt_explicit_version" 439 | vlog "Detected sbt version $sbt_version" 440 | 441 | [[ -n "$scala_version" ]] && vlog "Overriding scala version to $scala_version" 442 | 443 | # no args - alert them there's stuff in here 444 | (( argumentCount > 0 )) || { 445 | vlog "Starting $script_name: invoke with -help for other options" 446 | residual_args=( shell ) 447 | } 448 | 449 | # verify this is an sbt dir or -create was given 450 | [[ -r ./build.sbt || -d ./project || -n "$sbt_create" ]] || { 451 | cat <