├── samples ├── java │ └── demo │ │ ├── public │ │ ├── stylesheets │ │ │ └── main.css │ │ └── images │ │ │ └── favicon.png │ │ ├── project │ │ ├── build.properties │ │ ├── plugins.sbt │ │ └── Build.scala │ │ ├── conf │ │ ├── play.plugins │ │ ├── routes │ │ ├── application.conf │ │ └── securesocial.conf │ │ ├── README │ │ └── app │ │ ├── views │ │ ├── main.scala.html │ │ └── index.scala.html │ │ ├── service │ │ └── InMemoryUserService.java │ │ └── controllers │ │ └── Application.java └── scala │ └── demo │ ├── public │ ├── stylesheets │ │ └── main.css │ └── images │ │ └── favicon.png │ ├── project │ ├── build.properties │ ├── plugins.sbt │ └── Build.scala │ ├── conf │ ├── play.plugins │ ├── routes │ ├── application.conf │ └── securesocial.conf │ ├── README │ └── app │ ├── views │ ├── main.scala.html │ └── index.scala.html │ ├── controllers │ └── Application.scala │ └── service │ └── InMemoryUserService.scala ├── module-code ├── public │ ├── images │ │ ├── favicon.png │ │ └── providers │ │ │ ├── github.png │ │ │ ├── google.png │ │ │ ├── facebook.png │ │ │ ├── linkedin.png │ │ │ └── twitter.png │ └── bootstrap │ │ ├── img │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png │ │ ├── css │ │ ├── bootstrap-responsive.min.css │ │ └── bootstrap-responsive.css │ │ └── js │ │ └── bootstrap.min.js ├── app │ └── securesocial │ │ ├── views │ │ ├── inputFieldConstructor.scala.html │ │ ├── main.scala.html │ │ ├── protectedAction.scala.html │ │ ├── login.scala.html │ │ └── Registration │ │ │ └── signUp.scala.html │ │ ├── core │ │ ├── java │ │ │ ├── PasswordInfo.java │ │ │ ├── UserId.java │ │ │ ├── OAuth2Info.java │ │ │ ├── OAuth1Info.java │ │ │ ├── AuthenticationMethod.java │ │ │ ├── BaseUserService.java │ │ │ ├── SocialUser.java │ │ │ └── SecureSocial.java │ │ ├── AuthenticationException.scala │ │ ├── AccessDeniedException.scala │ │ ├── AuthenticationMethod.scala │ │ ├── ProviderRegistry.scala │ │ ├── providers │ │ │ ├── utils │ │ │ │ ├── GravatarHelper.scala │ │ │ │ └── PasswordHasher.scala │ │ │ ├── TwitterProvider.scala │ │ │ ├── UsernamePasswordProvider.scala │ │ │ ├── GoogleProvider.scala │ │ │ ├── LinkedInProvider.scala │ │ │ ├── GitHubProvider.scala │ │ │ └── FacebookProvider.scala │ │ ├── SocialUser.scala │ │ ├── UserService.scala │ │ ├── IdentityProvider.scala │ │ ├── OAuth1Provider.scala │ │ ├── SecureSocial.scala │ │ └── OAuth2Provider.scala │ │ └── controllers │ │ ├── LoginPage.scala │ │ └── Registration.scala └── conf │ ├── routes │ ├── securesocial │ └── defaults.conf │ └── messages ├── conf ├── play.plugins └── securesocial.conf ├── .gitignore ├── Readme-Icons.txt ├── README.textile └── LICENSE.txt /samples/java/demo/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/scala/demo/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/java/demo/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.11.3 2 | -------------------------------------------------------------------------------- /samples/scala/demo/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.11.3 2 | -------------------------------------------------------------------------------- /module-code/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/images/favicon.png -------------------------------------------------------------------------------- /samples/java/demo/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/samples/java/demo/public/images/favicon.png -------------------------------------------------------------------------------- /module-code/public/images/providers/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/images/providers/github.png -------------------------------------------------------------------------------- /module-code/public/images/providers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/images/providers/google.png -------------------------------------------------------------------------------- /samples/scala/demo/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/samples/scala/demo/public/images/favicon.png -------------------------------------------------------------------------------- /module-code/public/images/providers/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/images/providers/facebook.png -------------------------------------------------------------------------------- /module-code/public/images/providers/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/images/providers/linkedin.png -------------------------------------------------------------------------------- /module-code/public/images/providers/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/images/providers/twitter.png -------------------------------------------------------------------------------- /module-code/public/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /module-code/public/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marsmining/securesocial/master/module-code/public/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /conf/play.plugins: -------------------------------------------------------------------------------- 1 | 9999:db.InMemoryUserService 2 | 10000:securesocial.core.providers.TwitterProvider 3 | 10001:securesocial.core.providers.FacebookProvider 4 | 10002:securesocial.core.providers.GoogleProvider 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General temp files 2 | .DS_Store 3 | *.ser 4 | *.pyc 5 | 6 | # Files related to Play build or usage 7 | target 8 | logs 9 | 10 | # IDE and editors 11 | *~ 12 | .idea 13 | nbproject 14 | eclipse 15 | .amateras 16 | .settings 17 | .classpath 18 | .project 19 | *.iml 20 | *.ipr 21 | *.iws 22 | -------------------------------------------------------------------------------- /samples/java/demo/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 | // Use the Play sbt plugin for Play projects 8 | addSbtPlugin("play" % "sbt-plugin" % "2.0.3") 9 | -------------------------------------------------------------------------------- /Readme-Icons.txt: -------------------------------------------------------------------------------- 1 | The icons used in this module (except the MyOpenID icon) are part of the WPZOOM Social Networking Icon Set by WPZOOM designed by David Ferreira. 2 | 3 | They are licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License (http://creativecommons.org/licenses/by-nc-sa/3.0/). 4 | 5 | Please read more at: 6 | http://www.iconfinder.com/browse/iconset/WPZOOM_Social_Networking_Icon_Set/#readme 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/java/demo/conf/play.plugins: -------------------------------------------------------------------------------- 1 | 9998:service.InMemoryUserService 2 | 9999:securesocial.core.providers.utils.BCryptPasswordHasher 3 | 10000:securesocial.core.providers.TwitterProvider 4 | 10001:securesocial.core.providers.FacebookProvider 5 | 10002:securesocial.core.providers.GoogleProvider 6 | 10003:securesocial.core.providers.LinkedInProvider 7 | 10004:securesocial.core.providers.UsernamePasswordProvider 8 | 10005:securesocial.core.providers.GitHubProvider 9 | -------------------------------------------------------------------------------- /samples/scala/demo/conf/play.plugins: -------------------------------------------------------------------------------- 1 | 9998:service.InMemoryUserService 2 | 9999:securesocial.core.providers.utils.BCryptPasswordHasher 3 | 10000:securesocial.core.providers.TwitterProvider 4 | 10001:securesocial.core.providers.FacebookProvider 5 | 10002:securesocial.core.providers.GoogleProvider 6 | 10003:securesocial.core.providers.LinkedInProvider 7 | 10004:securesocial.core.providers.UsernamePasswordProvider 8 | 10005:securesocial.core.providers.GitHubProvider 9 | -------------------------------------------------------------------------------- /samples/scala/demo/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 | Resolver.file("Local Repository", file("/Play20/repository/local"))(Resolver.ivyStylePatterns) 7 | 8 | // Use the Play sbt plugin for Play projects 9 | addSbtPlugin("play" % "sbt-plugin" % "2.0.3") 10 | -------------------------------------------------------------------------------- /samples/java/demo/README: -------------------------------------------------------------------------------- 1 | Sample SecureSocial 2 Application (Java version) 2 | ================================================= 3 | To run this sample you need to: 4 | 5 | 1) Copy the contents of the module-code directory into modules/securesocial. If you are using a unix based 6 | OS you can do a symbolic link instead. 7 | 8 | 2) Create an application in the developer site of each provider you want to try and configure the OAuth 9 | settings in the securesocial.conf file. 10 | 11 | 3) Include securesocial.conf from application.conf 12 | 13 | -------------------------------------------------------------------------------- /samples/scala/demo/README: -------------------------------------------------------------------------------- 1 | Sample SecureSocial 2 Application (Scala version) 2 | ================================================= 3 | To run this sample you need to: 4 | 5 | 1) Copy the contents of the module-code directory into modules/securesocial. If you are using a unix based 6 | OS you can do a symbolic link instead. 7 | 8 | 2) Create an application in the developer site of each provider you want to try and configure the OAuth 9 | settings in the securesocial.conf file. 10 | 11 | 3) Include securesocial.conf from application.conf 12 | 13 | -------------------------------------------------------------------------------- /module-code/app/securesocial/views/inputFieldConstructor.scala.html: -------------------------------------------------------------------------------- 1 | @(elements: views.html.helper.FieldElements) 2 | 3 | @import play.api.i18n._ 4 | @import views.html.helper._ 5 | 6 |
7 | 8 |
9 | @elements.input 10 | @elements.errors(elements.lang).mkString(", ") 11 | @elements.infos(elements.lang).mkString(", ") 12 |
13 |
-------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/PasswordInfo.java: -------------------------------------------------------------------------------- 1 | package securesocial.core.java; 2 | 3 | /** 4 | * The password information 5 | */ 6 | public class PasswordInfo { 7 | /** 8 | * The hashed user password 9 | */ 10 | public String password; 11 | 12 | /** 13 | * The salt used to hash the password 14 | */ 15 | public String salt; 16 | 17 | public static PasswordInfo fromScala(securesocial.core.PasswordInfo scalaInfo) { 18 | PasswordInfo result = new PasswordInfo(); 19 | result.password = scalaInfo.password(); 20 | if ( scalaInfo.salt().isDefined() ) { 21 | result.salt = scalaInfo.salt().get(); 22 | } 23 | return result; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /module-code/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Login page 6 | GET /login securesocial.controllers.LoginPage.login 7 | GET /logout securesocial.controllers.LoginPage.logout 8 | 9 | # User Registration 10 | GET /signup securesocial.controllers.Registration.signUp 11 | POST /signup securesocial.controllers.Registration.handleSignUp 12 | 13 | # Providers entry points 14 | GET /authenticate/:provider securesocial.controllers.LoginPage.authenticate(provider) 15 | POST /authenticate/:provider securesocial.controllers.LoginPage.authenticateByPost(provider) 16 | 17 | 18 | 19 | # Map static resources from the /public folder to the /assets URL path 20 | GET /assets/*file controllers.Assets.at(path="/public", file) 21 | -------------------------------------------------------------------------------- /samples/scala/demo/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 |
22 | @content 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/AuthenticationException.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | /** 20 | * An exception thrown when there is an error in the authentication flow 21 | */ 22 | case class AuthenticationException() extends Exception 23 | -------------------------------------------------------------------------------- /samples/java/demo/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 |
22 | @content 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /module-code/app/securesocial/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 |
22 | @content 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/AccessDeniedException.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | /** 20 | * An exception thrown when the user denies access to the application 21 | * in the login page of the 3rd party service 22 | */ 23 | case class AccessDeniedException() extends Exception 24 | -------------------------------------------------------------------------------- /samples/scala/demo/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | 8 | # Map static resources from the /public folder to the /assets URL path 9 | GET /assets/*file controllers.Assets.at(path="/public", file) 10 | 11 | # Login page 12 | GET /login securesocial.controllers.LoginPage.login 13 | GET /logout securesocial.controllers.LoginPage.logout 14 | 15 | # User Registration 16 | GET /signup securesocial.controllers.Registration.signUp 17 | POST /signup securesocial.controllers.Registration.handleSignUp 18 | 19 | # Providers entry points 20 | GET /authenticate/:provider securesocial.controllers.LoginPage.authenticate(provider) 21 | POST /authenticate/:provider securesocial.controllers.LoginPage.authenticateByPost(provider) 22 | 23 | -------------------------------------------------------------------------------- /samples/scala/demo/app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package controllers 18 | 19 | import play.api.mvc._ 20 | 21 | object Application extends Controller with securesocial.core.SecureSocial { 22 | 23 | def index = SecuredAction() { implicit request => 24 | Ok(views.html.index(request.user)) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /samples/java/demo/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | GET /userAware controllers.Application.userAware 8 | 9 | # Map static resources from the /public folder to the /assets URL path 10 | GET /assets/*file controllers.Assets.at(path="/public", file) 11 | 12 | # Login page 13 | GET /login securesocial.controllers.LoginPage.login 14 | GET /logout securesocial.controllers.LoginPage.logout 15 | 16 | # User Registration 17 | GET /signup securesocial.controllers.Registration.signUp 18 | POST /signup securesocial.controllers.Registration.handleSignUp 19 | 20 | GET /authenticate/:provider securesocial.controllers.LoginPage.authenticate(provider) 21 | POST /authenticate/:provider securesocial.controllers.LoginPage.authenticateByPost(provider) 22 | 23 | 24 | -------------------------------------------------------------------------------- /module-code/conf/securesocial/defaults.conf: -------------------------------------------------------------------------------- 1 | ##################################################################################### 2 | # 3 | # SecureSocial 2 Defaults 4 | # 5 | ##################################################################################### 6 | 7 | securesocial { 8 | onLoginGoTo=/ 9 | onLogoutGoTo=/login 10 | 11 | twitter { 12 | requestTokenUrl="https://twitter.com/oauth/request_token" 13 | accessTokenUrl="https://twitter.com/oauth/access_token" 14 | authorizationUrl="https://twitter.com/oauth/authenticate" 15 | } 16 | 17 | facebook { 18 | authorizationUrl="https://graph.facebook.com/oauth/authorize" 19 | accessTokenUrl="https://graph.facebook.com/oauth/access_token" 20 | scope=email 21 | } 22 | 23 | google { 24 | authorizationUrl="https://accounts.google.com/o/oauth2/auth" 25 | accessTokenUrl="https://accounts.google.com/o/oauth2/token" 26 | scope="https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" 27 | } 28 | } -------------------------------------------------------------------------------- /samples/java/demo/project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import PlayProject._ 4 | 5 | object ApplicationBuild extends Build { 6 | 7 | val appName = "ssdemo-java" 8 | val appVersion = "1.0-SNAPSHOT" 9 | 10 | val ssDependencies = Seq( 11 | // Add your project dependencies here, 12 | "com.typesafe" %% "play-plugins-util" % "2.0.1", 13 | "org.mindrot" % "jbcrypt" % "0.3m" 14 | ) 15 | 16 | val secureSocial = PlayProject( 17 | "securesocial", appVersion, ssDependencies, mainLang = SCALA, path = file("modules/securesocial") 18 | ).settings( 19 | resolvers ++= Seq( 20 | "jBCrypt Repository" at "http://repo1.maven.org/maven2/org/", 21 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" 22 | ) 23 | ) 24 | 25 | val appDependencies = Seq() 26 | val main = PlayProject(appName, appVersion, appDependencies, mainLang = JAVA).settings( 27 | // Add your own project settings here 28 | ).dependsOn(secureSocial).aggregate(secureSocial) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /samples/scala/demo/project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import PlayProject._ 4 | 5 | object ApplicationBuild extends Build { 6 | 7 | val appName = "ssdemo-scala" 8 | val appVersion = "1.0-SNAPSHOT" 9 | 10 | val ssDependencies = Seq( 11 | // Add your project dependencies here, 12 | "com.typesafe" %% "play-plugins-util" % "2.0.1", 13 | "org.mindrot" % "jbcrypt" % "0.3m" 14 | ) 15 | 16 | val secureSocial = PlayProject( 17 | "securesocial", appVersion, ssDependencies, mainLang = SCALA, path = file("modules/securesocial") 18 | ).settings( 19 | resolvers ++= Seq( 20 | "jBCrypt Repository" at "http://repo1.maven.org/maven2/org/", 21 | "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" 22 | ) 23 | ) 24 | 25 | val appDependencies = Seq() 26 | val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings( 27 | // Add your own project settings here 28 | ).dependsOn(secureSocial).aggregate(secureSocial) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /module-code/conf/messages: -------------------------------------------------------------------------------- 1 | # App name 2 | securesocial.appName=SecureSocial 2 3 | 4 | # Login page 5 | securesocial.login.title=Login 6 | securesocial.login.instructions=Use your existing account on one of the following networks to log in. 7 | securesocial.login.accessDenied=You denied access to your account. Please grant it to log in. 8 | securesocial.login.useEmailAndPassword=Or login using a username and password. 9 | securesocial.login.signUp=If you don't have an account with us yet you can sign up 10 | securesocial.login.here=here 11 | securesocial.login.invalidCredentials=Invalid username and password combination 12 | 13 | # Sign up page 14 | securesocial.signup.title=Signup 15 | securesocial.signup.username=Username 16 | securesocial.signup.fullName=Full Name 17 | securesocial.signup.email1=Email 18 | securesocial.signup.email2=Email confirmation 19 | securesocial.signup.password1=Password 20 | securesocial.signup.password2=Password confirmation 21 | securesocial.signup.createAccount=Create Account 22 | securesocial.signup.cancel=Cancel 23 | 24 | # 25 | securesocial.loginRequired=You need to log in to access that page. -------------------------------------------------------------------------------- /conf/securesocial.conf: -------------------------------------------------------------------------------- 1 | ##################################################################################### 2 | # 3 | # SecureSocial 2 Settings 4 | # 5 | ##################################################################################### 6 | 7 | include "securesocial/defaults.conf" 8 | 9 | securesocial { 10 | # 11 | # Where to redirect the user if SecureSocial can't figure that out from 12 | # the request that led the use to the login page 13 | # 14 | # onLoginGoTo=/ 15 | 16 | # 17 | # Where to redirect the user when he logs out. If not set SecureSocial will redirect to the login page 18 | # 19 | # onLogoutGoTo=/login 20 | 21 | twitter { 22 | consumerKey=your_consumer_key 23 | consumerSecret=your_consumer_secret 24 | } 25 | 26 | facebook { 27 | clientId=your_client_id 28 | clientSecret=your_client_secret 29 | # this scope is the minimum SecureSocial requires. You can add more if required by your app. 30 | # scope=email 31 | } 32 | 33 | google { 34 | clientId=your_client_id 35 | clientSecret=your_client_secret 36 | # scope="https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/UserId.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | /** 20 | * A class to uniquely identify users. This combines the id the user has on 21 | * an external service (eg: twitter, facebook) with a provider. 22 | */ 23 | public class UserId { 24 | /** 25 | * The user id on the external provider 26 | */ 27 | public String id; 28 | 29 | /** 30 | * The provider id 31 | */ 32 | public String provider; 33 | } 34 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/AuthenticationMethod.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | /** 20 | * A class representing an authentication method 21 | */ 22 | case class AuthenticationMethod(method: String) 23 | 24 | /** 25 | * Authentication methods used by the identity providers 26 | */ 27 | object AuthenticationMethod { 28 | val OAuth1 = AuthenticationMethod("oauth1") 29 | val OAuth2 = AuthenticationMethod("oauth2") 30 | val OpenId = AuthenticationMethod("openId") 31 | val UserPassword = AuthenticationMethod("userPassword") 32 | } 33 | 34 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. SecureSocial Module for Play 2 2 | 3 | SecureSocial allows you to add an authentication UI to your app that works with services based on OAuth1, OAuth2 and OpenID protocols. SecureSocial provides Scala and Java APIs so you can integrate it using your preferred language. 4 | 5 | The following services are supported in this release: 6 | 7 | * Twitter (OAuth1) 8 | * Facebook (OAuth2) 9 | * Google (OAuth2) 10 | * LinkedIn (OAuth1) 11 | * GitHub (OAuth2) 12 | 13 | The following are going to be supported shortly: 14 | 15 | * Yahoo 16 | * Foursquare 17 | * MyOpenID 18 | * Wordpress 19 | * Username and Password (in progress) 20 | 21 | The module does not depend on any external Java libray. It relies only on what the Play! Framework provides and uses the awesome Bootstrap toolkit from Twitter to style the UI. 22 | 23 | h1. SecureSocial Module for Play 1.x 24 | 25 | The old version of SecureSocial is under the 1.x branch now. The 'master' branch is for the Play 2 version only. 26 | 27 | Source code is available at https://github.com/jaliss/securesocial 28 | Written by Jorge Aliss (@jaliss) 29 | 30 | h2. Licence 31 | 32 | SecureSocial is distributed under the "Apache 2 licence":http://www.apache.org/licenses/LICENSE-2.0.html. 33 | -------------------------------------------------------------------------------- /samples/scala/demo/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(user: securesocial.core.SocialUser) 2 | 3 | @main("SecureSocial - Sample Protected Page") { 4 | 7 | 8 |
9 |

User Details

10 | 11 | 17 | 18 | @user.oAuth1Info.map { info => 19 |

OAuth1 Info

20 | 21 | 25 | } 26 | 27 | @user.oAuth2Info.map { info => 28 |

OAuth2 Info

29 | 30 | 36 | } 37 |
38 | Logout 39 |
40 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/views/protectedAction.scala.html: -------------------------------------------------------------------------------- 1 | @(user: securesocial.core.SocialUser) 2 | 3 | @main("SecureSocial - Sample Protected Page") { 4 | 7 | 8 |
9 |

User Details

10 | 11 | 17 | 18 | @user.oAuth1Info.map { info => 19 |

OAuth1 Info

20 | 21 | 25 | } 26 | 27 | @user.oAuth2Info.map { info => 28 |

OAuth2 Info

29 | 30 | 36 | } 37 |
38 | Logout 39 |
40 | } -------------------------------------------------------------------------------- /samples/scala/demo/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 | application.secret="HPP/7@=BndE@Vblc^pepK4`<5Qjp_UmI@==Pmg9dGQ5Eorw>/eGJPphIesq`GFD5" 9 | 10 | # The application languages 11 | # ~~~~~ 12 | application.langs="en" 13 | 14 | # Global object class 15 | # ~~~~~ 16 | # Define the Global object class for this application. 17 | # Default to Global in the root package. 18 | # global=Global 19 | 20 | # Database configuration 21 | # ~~~~~ 22 | # You can declare as many datasources as you want. 23 | # By convention, the default datasource is named `default` 24 | # 25 | # db.default.driver=org.h2.Driver 26 | # db.default.url="jdbc:h2:mem:play" 27 | # db.default.user=sa 28 | # db.default.password= 29 | 30 | # Evolutions 31 | # ~~~~~ 32 | # You can disable evolutions if needed 33 | # evolutionplugin=disabled 34 | 35 | # Logger 36 | # ~~~~~ 37 | # You can also configure logback (http://logback.qos.ch/), by providing a logger.xml file in the conf directory . 38 | 39 | # Root logger: 40 | logger.root=ERROR 41 | 42 | # Logger used by the framework: 43 | logger.play=INFO 44 | 45 | # Logger provided to your application: 46 | logger.application=DEBUG 47 | 48 | include "securesocial.conf" -------------------------------------------------------------------------------- /samples/scala/demo/app/service/InMemoryUserService.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package service 18 | 19 | import play.api.Application 20 | import securesocial.core.{UserServicePlugin, UserId, SocialUser} 21 | 22 | 23 | /** 24 | * A Sample In Memory user service in Scala 25 | * 26 | * IMPORTANT: This is just a sample and not suitable for a production environment since 27 | * it stores everything in memory. 28 | */ 29 | class InMemoryUserService(application: Application) extends UserServicePlugin(application) { 30 | private var users = Map[String, SocialUser]() 31 | 32 | def find(id: UserId) = { 33 | users.get(id.id + id.providerId) 34 | } 35 | 36 | def save(user: SocialUser) { 37 | users = users + (user.id.id + user.id.providerId -> user) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/java/demo/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(user: securesocial.core.java.SocialUser) 2 | @main("SecureSocial - Sample Protected Page") { 3 | 6 | 7 |
8 |

User Details

9 | 10 | 16 | 17 | @if( user.oAuth1Info != null ) { 18 |

OAuth1 Info

19 | 20 | 24 | } 25 | 26 | @if( user.oAuth2Info != null) { 27 |

OAuth2 Info

28 | 29 | 35 | } 36 |
37 | Logout 38 |
39 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/core/ProviderRegistry.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import play.api.Logger 20 | 21 | /** 22 | * A registry for all the providers. Each provider is a plugin that will register itself here 23 | * at application start time. 24 | */ 25 | object ProviderRegistry { 26 | private var providers = Map[String, IdentityProvider]() 27 | 28 | def register(provider: IdentityProvider) { 29 | if ( providers.contains(provider.providerId) ) { 30 | throw new RuntimeException("There is already a provider registered for type: %s".format(provider.providerId)) 31 | } 32 | 33 | val p = (provider.providerId, provider) 34 | providers += p 35 | Logger.info("Registered Identity Provider: %s".format(provider.providerId)) 36 | } 37 | 38 | def get(providerId: String) = providers.get(providerId) 39 | 40 | def all() = providers 41 | } 42 | -------------------------------------------------------------------------------- /samples/java/demo/app/service/InMemoryUserService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package service; 18 | 19 | import play.Application; 20 | import securesocial.core.java.BaseUserService; 21 | import securesocial.core.java.SocialUser; 22 | import securesocial.core.java.UserId; 23 | 24 | import java.util.HashMap; 25 | 26 | /** 27 | * A Sample In Memory user service in Java 28 | */ 29 | public class InMemoryUserService extends BaseUserService { 30 | private HashMap users = new HashMap(); 31 | 32 | public InMemoryUserService(Application application) { 33 | super(application); 34 | } 35 | 36 | @Override 37 | public void doSave(SocialUser user) { 38 | users.put(user.id.id + user.id.provider, user); 39 | } 40 | 41 | @Override 42 | public SocialUser doFind(UserId userId) { 43 | return users.get(userId.id + userId.provider); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/utils/GravatarHelper.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers.utils 18 | 19 | import java.security.MessageDigest 20 | import play.api.libs.ws.WS 21 | 22 | object GravatarHelper { 23 | val GravatarUrl = "http://www.gravatar.com/avatar/%s?d=404" 24 | val Md5 = "MD5" 25 | 26 | def avatarFor(email: String): Option[String] = { 27 | hash(email).map( hash => { 28 | val url = GravatarUrl.format(hash) 29 | WS.url(url).get().await(10000).fold( 30 | onError => None, 31 | onSuccess => if (onSuccess.status == 200) Some(url) else None 32 | ) 33 | }).getOrElse(None) 34 | } 35 | 36 | private def hash(email: String): Option[String] = { 37 | val s = email.trim.toLowerCase 38 | if ( s.length > 0 ) { 39 | val out = MessageDigest.getInstance(Md5).digest(s.getBytes) 40 | Some(BigInt(1, out).toString(16)) 41 | } else { 42 | None 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /samples/java/demo/app/controllers/Application.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package controllers; 18 | 19 | import play.mvc.Controller; 20 | import play.mvc.Result; 21 | import securesocial.core.java.SecureSocial; 22 | import securesocial.core.java.SocialUser; 23 | import views.html.index; 24 | 25 | /** 26 | * A sample controller 27 | */ 28 | public class Application extends Controller { 29 | /** 30 | * This action only gets called if the user is logged in. 31 | * 32 | * @return 33 | */ 34 | @SecureSocial.Secured 35 | public static Result index() { 36 | SocialUser user = (SocialUser) ctx().args.get(SecureSocial.USER_KEY); 37 | return ok(index.render(user)); 38 | } 39 | 40 | @SecureSocial.UserAware 41 | public static Result userAware() { 42 | SocialUser user = (SocialUser) ctx().args.get(SecureSocial.USER_KEY); 43 | final String userName = user != null ? user.displayName : "guest"; 44 | return ok("Hello " + userName + ", you are seeing a public page"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/java/demo/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 | application.secret=":hcl=cwZ8^kLxj^rC/eFNMBK_Sku/9OHbEOcHCUCORy0E4^<9e" 9 | 10 | # The application languages 11 | # ~~~~~ 12 | application.langs="en" 13 | 14 | # Global object class 15 | # ~~~~~ 16 | # Define the Global object class for this application. 17 | # Default to Global in the root package. 18 | # global=Global 19 | 20 | # Database configuration 21 | # ~~~~~ 22 | # You can declare as many datasources as you want. 23 | # By convention, the default datasource is named `default` 24 | # 25 | # db.default.driver=org.h2.Driver 26 | # db.default.url="jdbc:h2:mem:play" 27 | # db.default.user=sa 28 | # db.default.password= 29 | # 30 | # You can expose this datasource via JNDI if needed (Useful for JPA) 31 | # db.default.jndiName=DefaultDS 32 | 33 | # Evolutions 34 | # ~~~~~ 35 | # You can disable evolutions if needed 36 | # evolutionplugin=disabled 37 | 38 | # Ebean configuration 39 | # ~~~~~ 40 | # You can declare as many Ebean servers as you want. 41 | # By convention, the default server is named `default` 42 | # 43 | # ebean.default="models.*" 44 | 45 | # Logger 46 | # ~~~~~ 47 | # You can also configure logback (http://logback.qos.ch/), by providing a logger.xml file in the conf directory . 48 | 49 | # Root logger: 50 | logger.root=ERROR 51 | 52 | # Logger used by the framework: 53 | logger.play=INFO 54 | 55 | # Logger provided to your application: 56 | logger.application=DEBUG 57 | 58 | include "securesocial.conf" -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/OAuth2Info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | /** 20 | * The OAuth2 information 21 | */ 22 | public class OAuth2Info { 23 | public String accessToken; 24 | public String tokenType; 25 | public Integer expiresIn; 26 | public String refreshToken; 27 | 28 | /** 29 | * This translates the scala version of OAuth2Info to the Java version 30 | * @param scalaInfo 31 | * @return 32 | */ 33 | public static OAuth2Info fromScala(securesocial.core.OAuth2Info scalaInfo) { 34 | OAuth2Info result = new OAuth2Info(); 35 | result.accessToken = scalaInfo.accessToken(); 36 | 37 | if ( scalaInfo.tokenType().isDefined() ) { 38 | result.tokenType = scalaInfo.tokenType().get(); 39 | } 40 | 41 | if ( scalaInfo.expiresIn().isDefined() ) { 42 | result.expiresIn = (Integer) scalaInfo.expiresIn().get(); 43 | } 44 | 45 | if ( scalaInfo.refreshToken().isDefined() ) { 46 | result.refreshToken = scalaInfo.refreshToken().get(); 47 | } 48 | 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/SocialUser.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import play.api.libs.oauth.ServiceInfo 20 | 21 | /** 22 | * A User that logs in using one of the IdentityProviders 23 | */ 24 | case class SocialUser(id: UserId, displayName: String, email: Option[String], 25 | avatarUrl: Option[String], authMethod: AuthenticationMethod, 26 | isEmailVerified: Boolean = false, 27 | oAuth1Info: Option[OAuth1Info] = None, 28 | oAuth2Info: Option[OAuth2Info] = None, 29 | passwordInfo: Option[PasswordInfo] = None) 30 | 31 | /** 32 | * The ID of a Social user 33 | * 34 | * @param id the id on the provider the user came from (eg: twitter, facebook) 35 | * @param providerId the provider the used to sign in 36 | */ 37 | case class UserId(id: String, providerId: String) 38 | 39 | /** 40 | * The OAuth 1 details 41 | * 42 | * @param serviceInfo 43 | * @param token 44 | * @param secret 45 | */ 46 | case class OAuth1Info(serviceInfo: ServiceInfo, token: String, secret: String) 47 | 48 | case class OAuth2Info(accessToken: String, tokenType: Option[String] = None, 49 | expiresIn: Option[Int] = None, refreshToken: Option[String] = None) 50 | 51 | case class PasswordInfo(password: String, salt: Option[String] = None) 52 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/OAuth1Info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | import play.api.libs.oauth.ServiceInfo; 20 | 21 | /** 22 | * The OAuth1 information 23 | */ 24 | public class OAuth1Info { 25 | /** 26 | * The ServiceInfo holds the information to identify an oauth provider. 27 | * 28 | * Note: this value does not need to be persisted by UserService since it is set automatically 29 | * in the SecureSocial Controller for each request that needs it. 30 | */ 31 | public ServiceInfo serviceInfo; 32 | 33 | /** 34 | * The token returned by the OAuth provider 35 | */ 36 | public String token; 37 | 38 | /** 39 | * The secret returned by the OAuth provider 40 | */ 41 | public String secret; 42 | 43 | /** 44 | * This translates the scala version of OAuth1Info to the Java version 45 | * 46 | * @param info 47 | * @return 48 | */ 49 | public static OAuth1Info fromScala(securesocial.core.OAuth1Info info) { 50 | OAuth1Info result = new OAuth1Info(); 51 | result.token = info.token(); 52 | result.secret = info.secret(); 53 | // I'm not providing a serviceInfo wrapper for now (I think this is going to be added to Play) 54 | result.serviceInfo = info.serviceInfo(); 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/TwitterProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers 18 | 19 | import securesocial.core._ 20 | import play.api.mvc.{Request, Results, Result} 21 | import play.api.libs.oauth.{RequestToken, OAuthCalculator} 22 | import play.api.libs.ws.{Response, WS} 23 | import play.api.{Application, Logger} 24 | 25 | 26 | /** 27 | * A Twitter Provider 28 | */ 29 | class TwitterProvider(application: Application) extends OAuth1Provider(application) { 30 | 31 | 32 | override def providerId = TwitterProvider.Twitter 33 | 34 | override def fillProfile(user: SocialUser): SocialUser = { 35 | //var result = user 36 | val oauthInfo = user.oAuth1Info.get 37 | val call = WS.url(TwitterProvider.VerifyCredentials).sign( 38 | OAuthCalculator(oauthInfo.serviceInfo.key, 39 | RequestToken(oauthInfo.token, oauthInfo.secret))).get() 40 | call.await(10000).fold( 41 | onError => { 42 | Logger.error("timed out waiting for Twitter") 43 | throw new AuthenticationException() 44 | }, 45 | response => 46 | { 47 | val me = response.json 48 | val id = (me \ "id").as[Int] 49 | val name = (me \ "name").as[String] 50 | val profileImage = (me \ "profile_image_url").asOpt[String] 51 | user.copy(id = UserId(id.toString, providerId), displayName = name, avatarUrl = profileImage) 52 | } 53 | ) 54 | } 55 | } 56 | 57 | object TwitterProvider { 58 | val VerifyCredentials = "https://api.twitter.com/1/account/verify_credentials.json" 59 | val Twitter = "twitter" 60 | } 61 | -------------------------------------------------------------------------------- /samples/java/demo/conf/securesocial.conf: -------------------------------------------------------------------------------- 1 | ##################################################################################### 2 | # 3 | # SecureSocial 2 Settings 4 | # 5 | ##################################################################################### 6 | 7 | securesocial { 8 | # 9 | # Where to redirect the user if SecureSocial can't figure that out from 10 | # the request that led the use to the login page 11 | # 12 | onLoginGoTo=/ 13 | 14 | # 15 | # Where to redirect the user when he logs out. If not set SecureSocial will redirect to the login page 16 | # 17 | onLogoutGoTo=/login 18 | 19 | # 20 | # If your app sits behind a HTTP reverse proxy, define the base URL here. 21 | # 22 | # callbackBaseUrl="http://mydomain.com" 23 | 24 | twitter { 25 | requestTokenUrl="https://twitter.com/oauth/request_token" 26 | accessTokenUrl="https://twitter.com/oauth/access_token" 27 | authorizationUrl="https://twitter.com/oauth/authenticate" 28 | consumerKey=your_consumer_key 29 | consumerSecret=your_consumer_secret 30 | } 31 | 32 | facebook { 33 | authorizationUrl="https://graph.facebook.com/oauth/authorize" 34 | accessTokenUrl="https://graph.facebook.com/oauth/access_token" 35 | clientId=your_client_id 36 | clientSecret=your_client_secret 37 | # this scope is the minimum SecureSocial requires. You can add more if required by your app. 38 | scope=email 39 | } 40 | 41 | google { 42 | authorizationUrl="https://accounts.google.com/o/oauth2/auth" 43 | accessTokenUrl="https://accounts.google.com/o/oauth2/token" 44 | clientId=your_client_id 45 | clientSecret=your_client_secret 46 | scope="https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" 47 | } 48 | 49 | linkedin { 50 | requestTokenUrl="https://api.linkedin.com/uas/oauth/requestToken" 51 | accessTokenUrl="https://api.linkedin.com/uas/oauth/accessToken" 52 | authorizationUrl="https://api.linkedin.com/uas/oauth/authenticate" 53 | consumerKey=your_consumer_key 54 | consumerSecret=your_consumer_secret 55 | } 56 | 57 | github { 58 | authorizationUrl="https://github.com/login/oauth/authorize" 59 | accessTokenUrl="https://github.com/login/oauth/access_token" 60 | clientId=your_client_id 61 | clientSecret=your_client_secret 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/scala/demo/conf/securesocial.conf: -------------------------------------------------------------------------------- 1 | ##################################################################################### 2 | # 3 | # SecureSocial 2 Settings 4 | # 5 | ##################################################################################### 6 | 7 | securesocial { 8 | # 9 | # Where to redirect the user if SecureSocial can't figure that out from 10 | # the request that led the use to the login page 11 | # 12 | onLoginGoTo=/ 13 | 14 | # 15 | # Where to redirect the user when he logs out. If not set SecureSocial will redirect to the login page 16 | # 17 | onLogoutGoTo=/login 18 | 19 | # 20 | # If your app sits behind a HTTP reverse proxy, define the base URL here. 21 | # 22 | # callbackBaseUrl="http://mydomain.com" 23 | 24 | twitter { 25 | requestTokenUrl="https://twitter.com/oauth/request_token" 26 | accessTokenUrl="https://twitter.com/oauth/access_token" 27 | authorizationUrl="https://twitter.com/oauth/authenticate" 28 | consumerKey=your_consumer_key 29 | consumerSecret=your_consumer_secret 30 | } 31 | 32 | facebook { 33 | authorizationUrl="https://graph.facebook.com/oauth/authorize" 34 | accessTokenUrl="https://graph.facebook.com/oauth/access_token" 35 | clientId=your_client_id 36 | clientSecret=your_client_secret 37 | # this scope is the minimum SecureSocial requires. You can add more if required by your app. 38 | scope=email 39 | } 40 | 41 | google { 42 | authorizationUrl="https://accounts.google.com/o/oauth2/auth" 43 | accessTokenUrl="https://accounts.google.com/o/oauth2/token" 44 | clientId=your_client_id 45 | clientSecret=your_client_secret 46 | scope="https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" 47 | } 48 | 49 | linkedin { 50 | requestTokenUrl="https://api.linkedin.com/uas/oauth/requestToken" 51 | accessTokenUrl="https://api.linkedin.com/uas/oauth/accessToken" 52 | authorizationUrl="https://api.linkedin.com/uas/oauth/authenticate" 53 | consumerKey=your_consumer_key 54 | consumerSecret=your_consumer_secret 55 | } 56 | 57 | github { 58 | authorizationUrl="https://github.com/login/oauth/authorize" 59 | accessTokenUrl="https://github.com/login/oauth/access_token" 60 | clientId=your_client_id 61 | clientSecret=your_client_secret 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /module-code/app/securesocial/views/login.scala.html: -------------------------------------------------------------------------------- 1 | @(providers: Iterable[securesocial.core.IdentityProvider], loginForm: Form[(String,String)], errorMsg: Option[String] = None) 2 | 3 | @import helper._ 4 | @implicitFieldConstructor = @{ FieldConstructor(securesocial.views.html.inputFieldConstructor.f) } 5 | 6 | @main(Messages("securesocial.login.title")) { 7 | 10 | 11 | @errorMsg.map { msg => 12 |
13 | @Messages(msg) 14 |
15 | } 16 | 17 |
18 |

@Messages("securesocial.login.instructions")

19 | 20 |

21 | @for(p <- providers if p.authMethod != securesocial.core.AuthenticationMethod.UserPassword) { 22 | @defining( "images/providers/%s.png".format(p.providerId) ) { imageUrl => 23 | 24 | } 25 | } 26 |

27 |
28 | 29 |
30 |

@Messages("securesocial.login.useEmailAndPassword")

31 | 32 | @helper.form(action = securesocial.controllers.routes.LoginPage.authenticateByPost("userpass"), 'class -> "form-horizontal", 'autocomplete -> "off") { 33 |
34 | 35 | @helper.inputText( 36 | loginForm("username"), 37 | '_label -> Messages("securesocial.signup.username"), 38 | 'class -> "input-xlarge" 39 | ) 40 | 41 | @helper.inputPassword( 42 | loginForm("password"), 43 | '_label -> Messages("securesocial.signup.password1"), 44 | 'class -> "input-xlarge" 45 | ) 46 | 47 |
48 | 49 | @Messages("securesocial.signup.cancel") 50 |
51 | 52 |
53 | } 54 |
55 | 56 | 57 |
58 |

@Messages("securesocial.login.signUp") @Messages("securesocial.login.here")

59 |
60 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/views/Registration/signUp.scala.html: -------------------------------------------------------------------------------- 1 | @(signUpForm:Form[securesocial.controllers.Registration.RegistrationInfo])(implicit flash: play.api.mvc.Flash) 2 | @import helper._ 3 | @implicitFieldConstructor = @{ FieldConstructor(securesocial.views.html.inputFieldConstructor.f) } 4 | 5 | @securesocial.views.html.main( Messages("securesocial.signup.title") ) { 6 | 9 | 10 | @flash.get("error").map { msg => 11 |
12 | @Messages(msg) 13 |
14 | } 15 | 16 | @helper.form(action = securesocial.controllers.routes.Registration.handleSignUp(), 'class -> "form-horizontal", 'autocomplete -> "off") { 17 |
18 | 19 | @helper.inputText( 20 | signUpForm("userName"), 21 | '_label -> Messages("securesocial.signup.username"), 22 | 'class -> "input-xlarge" 23 | ) 24 | 25 | @helper.inputText( 26 | signUpForm("fullName"), 27 | '_label -> Messages("securesocial.signup.fullName"), 28 | 'class -> "input-xlarge" 29 | ) 30 | 31 | @helper.inputText( 32 | signUpForm("email.email1"), 33 | '_label -> Messages("securesocial.signup.email1"), 34 | 'class -> "input-xlarge" 35 | ) 36 | 37 | @helper.inputText( 38 | signUpForm("email.email2"), 39 | '_label -> Messages("securesocial.signup.email2"), 40 | 'class -> "input-xlarge" 41 | ) 42 | 43 | @helper.inputPassword( 44 | signUpForm("password.password1"), 45 | '_label -> Messages("securesocial.signup.password1"), 46 | 'class -> "input-xlarge" 47 | ) 48 | 49 | @helper.inputPassword( 50 | signUpForm("password.password2"), 51 | '_label -> Messages("securesocial.signup.password2"), 52 | 'class -> "input-xlarge" 53 | ) 54 | 55 |
56 | 57 | @Messages("securesocial.signup.cancel") 58 |
59 |
60 | } 61 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/AuthenticationMethod.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | /** 20 | * Authentication methods used by the identity providers 21 | */ 22 | public enum AuthenticationMethod { 23 | OAUTH1, 24 | OAUTH2, 25 | OPENID, 26 | USERNAME_PASSWORD; 27 | 28 | public static AuthenticationMethod fromScala(securesocial.core.AuthenticationMethod scalaMethod) { 29 | final AuthenticationMethod result; 30 | 31 | if ( scalaMethod.equals( securesocial.core.AuthenticationMethod.OAuth1()) ) { 32 | result = OAUTH1; 33 | } else if ( scalaMethod.equals(securesocial.core.AuthenticationMethod.OAuth2())) { 34 | result = OAUTH2; 35 | } else if ( scalaMethod.equals(securesocial.core.AuthenticationMethod.OpenId())) { 36 | result = OPENID; 37 | } else if ( scalaMethod.equals(securesocial.core.AuthenticationMethod.UserPassword())) { 38 | result = USERNAME_PASSWORD; 39 | } else { 40 | throw new RuntimeException("Unknown authentication method: " + scalaMethod.toString()); 41 | } 42 | return result; 43 | } 44 | 45 | public static securesocial.core.AuthenticationMethod toSala(AuthenticationMethod method) { 46 | securesocial.core.AuthenticationMethod result = null; 47 | 48 | switch (method) { 49 | case OAUTH1: 50 | result = securesocial.core.AuthenticationMethod.OAuth1(); 51 | break; 52 | case OAUTH2: 53 | result = securesocial.core.AuthenticationMethod.OAuth2(); 54 | break; 55 | case OPENID: 56 | result = securesocial.core.AuthenticationMethod.OpenId(); 57 | break; 58 | case USERNAME_PASSWORD: 59 | result = securesocial.core.AuthenticationMethod.UserPassword(); 60 | break; 61 | } 62 | return result; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/UsernamePasswordProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers 18 | 19 | import play.api.data.Form 20 | import play.api.data.Forms._ 21 | import securesocial.core._ 22 | import play.api.mvc.{PlainResult, Results, Result, Request} 23 | import utils.PasswordHasher 24 | import play.api.{Play, Application} 25 | import Play.current 26 | import com.typesafe.plugin._ 27 | 28 | /** 29 | 30 | */ 31 | 32 | class UsernamePasswordProvider(application: Application) extends IdentityProvider(application) { 33 | 34 | def providerId = UsernamePasswordProvider.UsernamePassword 35 | 36 | def authMethod = AuthenticationMethod.UserPassword 37 | 38 | val InvalidCredentials = "securesocial.login.invalidCredentials" 39 | 40 | def doAuth[A]()(implicit request: Request[A]): Either[Result, SocialUser] = { 41 | val form = UsernamePasswordProvider.loginForm.bindFromRequest() 42 | form.fold( 43 | errors => Left(badRequest(errors)), 44 | credentials => { 45 | val userId = UserId(credentials._1, providerId) 46 | UserService.find(userId) match { 47 | case Some(user) if user.passwordInfo.isDefined && use[PasswordHasher].matches(user.passwordInfo.get, credentials._2) => 48 | Right(user) 49 | case _ => Left(badRequest(UsernamePasswordProvider.loginForm, Some(InvalidCredentials))) 50 | } 51 | } 52 | ) 53 | } 54 | 55 | private def badRequest(f: Form[(String,String)], msg: Option[String] = None): PlainResult = { 56 | Results.BadRequest(securesocial.views.html.login(ProviderRegistry.all().values, f, msg)) 57 | } 58 | 59 | def fillProfile(user: SocialUser) = { 60 | // nothing to do for this provider, the user should already have everything because it 61 | // was loaded from the backing store 62 | user 63 | } 64 | } 65 | 66 | object UsernamePasswordProvider { 67 | val UsernamePassword = "userpass" 68 | 69 | val loginForm = Form( 70 | tuple( 71 | "username" -> nonEmptyText, 72 | "password" -> nonEmptyText 73 | ) 74 | ) 75 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/core/UserService.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import play.api.{Logger, Plugin, Application} 20 | 21 | 22 | /** 23 | * A trait that provides the means to find and save users 24 | * for the SecureSocial module. 25 | * 26 | * @see DefaultUserService 27 | */ 28 | trait UserService { 29 | /** 30 | * Finds a SocialUser that maches the specified id 31 | * 32 | * @param id the user id 33 | * @return an optional user 34 | */ 35 | def find(id: UserId):Option[SocialUser] 36 | 37 | /** 38 | * Saves the user. This method gets called when a user logs in. 39 | * This is your chance to save the user information in your backing store. 40 | * @param user 41 | */ 42 | def save(user: SocialUser) 43 | } 44 | 45 | /** 46 | * Base class for the classes that implement UserService. Since this is a plugin it gets loaded 47 | * at application start time. Only one plugin of this type must be specified in the play.plugins file. 48 | * 49 | * @param application 50 | */ 51 | abstract class UserServicePlugin(application: Application) extends Plugin with UserService { 52 | /** 53 | * Registers this object so SecureSocial can invoke it. 54 | */ 55 | override def onStart() { 56 | UserService.setService(this) 57 | Logger.info("Registered UserService: " + this.getClass) 58 | } 59 | } 60 | 61 | /** 62 | * The UserService singleton 63 | */ 64 | object UserService { 65 | var delegate: Option[UserService] = None 66 | 67 | def setService(service: UserService) { 68 | delegate = Some(service) 69 | } 70 | 71 | def find(id: UserId):Option[SocialUser] = { 72 | delegate.map( _.find(id) ).getOrElse { 73 | notInitialized() 74 | None 75 | } 76 | } 77 | 78 | def save(user: SocialUser) { 79 | delegate.map( _.save(user) ).getOrElse { 80 | notInitialized() 81 | } 82 | } 83 | 84 | private def notInitialized() { 85 | Logger.error("UserService was not initialized. Make sure a UserService plugin is specified in your play.plugins file") 86 | throw new RuntimeException("UserService not initialized") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/GoogleProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers 18 | 19 | import play.api.libs.ws.WS 20 | import play.api.{Application, Logger} 21 | import play.api.libs.json.JsObject 22 | import securesocial.core._ 23 | 24 | 25 | /** 26 | * A Google OAuth2 Provider 27 | */ 28 | class GoogleProvider(application: Application) extends OAuth2Provider(application) { 29 | val UserInfoApi = "https://www.googleapis.com/oauth2/v1/userinfo?access_token=" 30 | val Error = "error" 31 | val Message = "message" 32 | val Type = "type" 33 | val Id = "id" 34 | val GivenName = "given_name" 35 | val FamilyName = "family_name" 36 | val Picture = "picture" 37 | val Email = "email" 38 | 39 | 40 | def providerId = GoogleProvider.Google 41 | 42 | def fillProfile(user: SocialUser): SocialUser = { 43 | val accessToken = user.oAuth2Info.get.accessToken 44 | val promise = WS.url(UserInfoApi + accessToken).get() 45 | 46 | promise.await(10000).fold( error => { 47 | Logger.error( "Error retrieving profile information", error) 48 | throw new AuthenticationException() 49 | }, response => { 50 | val me = response.json 51 | (me \ Error).asOpt[JsObject] match { 52 | case Some(error) => 53 | val message = (error \ Message).as[String] 54 | val errorType = ( error \ Type).as[String] 55 | Logger.error("Error retrieving profile information from Google. Error type = %s, message = %s" 56 | .format(errorType,message)) 57 | throw new AuthenticationException() 58 | case _ => 59 | val id = (me \ Id).as[String] 60 | val displayName = (me \ GivenName).as[String] + " " + (me \ FamilyName).as[String] 61 | val avatarUrl = ( me \ Picture).asOpt[String] 62 | val email = ( me \ Email).as[String] 63 | user.copy( 64 | id = UserId(id.toString, providerId), 65 | displayName = displayName, 66 | avatarUrl = avatarUrl, 67 | email = Some(email) 68 | ) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | object GoogleProvider { 75 | val Google = "google" 76 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/LinkedInProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers 18 | 19 | import securesocial.core._ 20 | import play.api.mvc.{Request, Results, Result} 21 | import play.api.libs.oauth.{RequestToken, OAuthCalculator} 22 | import play.api.libs.ws.{Response, WS} 23 | import play.api.{Application, Logger} 24 | 25 | 26 | /** 27 | * A LinkedIn Provider 28 | */ 29 | class LinkedInProvider(application: Application) extends OAuth1Provider(application) { 30 | 31 | 32 | override def providerId = LinkedInProvider.LinkedIn 33 | 34 | override def fillProfile(user: SocialUser): SocialUser = { 35 | val oauthInfo = user.oAuth1Info.get 36 | WS.url(LinkedInProvider.Api).sign(OAuthCalculator(oauthInfo.serviceInfo.key, 37 | RequestToken(oauthInfo.token, oauthInfo.secret))).get().await(10000).fold( 38 | onError => { 39 | Logger.error("timed out waiting for LinkedIn") 40 | throw new AuthenticationException() 41 | }, 42 | response => 43 | { 44 | val me = response.json 45 | (me \ "errorCode").asOpt[Int] match { 46 | case Some(error) => { 47 | val message = (me \ "message").asOpt[String] 48 | val requestId = (me \ "requestId").asOpt[String] 49 | val timestamp = (me \ "timestamp").asOpt[String] 50 | Logger.error( 51 | "Error retrieving information from LinkedIn. Error code: %s, requestId: %s, message: %s, timestamp: %s" 52 | format(error, message, requestId, timestamp) 53 | ) 54 | throw new AuthenticationException() 55 | } 56 | case _ => { 57 | val id = (me \ "id").as[String] 58 | val first = (me \ "firstName").asOpt[String] 59 | val last = (me \ "lastName").asOpt[String] 60 | val fullName = "%s %s".format(first.getOrElse(""), last.getOrElse("")) 61 | val avatarUrl = (me \ "pictureUrl").asOpt[String] 62 | user.copy(id = UserId(id, providerId), displayName = fullName, avatarUrl = avatarUrl) 63 | } 64 | } 65 | } 66 | ) 67 | } 68 | } 69 | 70 | object LinkedInProvider { 71 | val Api = "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,picture-url)?format=json" 72 | val LinkedIn = "linkedin" 73 | } 74 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/utils/PasswordHasher.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers.utils 18 | 19 | import securesocial.core.PasswordInfo 20 | import play.api.{Logger, Plugin, Application} 21 | import org.mindrot.jbcrypt._ 22 | 23 | /** 24 | * A trait that defines the password hasher interface 25 | */ 26 | 27 | trait PasswordHasher extends Plugin { 28 | /** 29 | * Hashes a password 30 | * 31 | * @param plainPassword the password to hash 32 | * @return a PasswordInfo containting the hashed password and optional salt 33 | */ 34 | def hash(plainPassword: String): PasswordInfo 35 | 36 | /** 37 | * Checks whether a supplied password matches the hashed one 38 | * 39 | * @param passwordInfo the password retrieved from the backing store (by means of UserService) 40 | * @param suppliedPassword the password supplied by the user trying to log in 41 | * @return true if the password matches, false otherwise. 42 | */ 43 | def matches(passwordInfo: PasswordInfo, suppliedPassword: String): Boolean 44 | } 45 | 46 | /** 47 | * The default password hasher based on BCrypt. 48 | */ 49 | class BCryptPasswordHasher(app: Application) extends PasswordHasher { 50 | val DefaultRounds = 10 51 | val RoundsProperty = "securesocial.passwordHasher.bcrypt.rounds" 52 | 53 | override def onStart() { 54 | Logger.info("Loaded BCryptPasswordHasher") 55 | } 56 | 57 | /** 58 | * Hashes a password. This implementation does not return the salt because it is not needed 59 | * to verify passwords later. Other implementations might need to return it so it gets saved in the 60 | * backing store. 61 | * 62 | * @param plainPassword the password to hash 63 | * @return a PasswordInfo containting the hashed password. 64 | */ 65 | def hash(plainPassword: String): PasswordInfo = { 66 | val logRounds = app.configuration.getInt(RoundsProperty).getOrElse(DefaultRounds) 67 | PasswordInfo(BCrypt.hashpw(plainPassword, BCrypt.gensalt(logRounds))) 68 | } 69 | 70 | /** 71 | * Checks if a password matches the hashed version 72 | * 73 | * @param passwordInfo the password retrieved from the backing store (by means of UserService) 74 | * @param suppliedPassword the password supplied by the user trying to log in 75 | * @return true if the password matches, false otherwise. 76 | */ 77 | def matches(passwordInfo: PasswordInfo, suppliedPassword: String):Boolean = { 78 | BCrypt.checkpw(suppliedPassword, passwordInfo.password) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/BaseUserService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | import play.Application; 20 | import play.libs.Scala; 21 | import scala.Option; 22 | 23 | import java.lang.reflect.Field; 24 | 25 | /** 26 | * A base user service for developers that want to write their UserService in Java. 27 | * The save() and find() methods handle the Scala<->Java conversions. 28 | * 29 | * Subclasses need to implement the doSave and doFind mehtods. 30 | * 31 | */ 32 | public abstract class BaseUserService extends securesocial.core.UserServicePlugin { 33 | 34 | public static final String APPLICATION = "application"; 35 | 36 | // a bit of black magic to be able to extend a Scala plugin :) 37 | private static play.api.Application toScala(Application app) { 38 | try { 39 | Field field = app.getClass().getDeclaredField(APPLICATION); 40 | field.setAccessible(true); 41 | return (play.api.Application) field.get(app); 42 | } catch (Exception e) { 43 | throw new RuntimeException("Unable to initialize user service", e); 44 | } 45 | } 46 | 47 | public BaseUserService(Application application) { 48 | super(toScala(application)); 49 | } 50 | 51 | /** 52 | * Finds a SocialUser that maches the specified id 53 | * 54 | * @param id the user id 55 | * @return an optional user 56 | */ 57 | @Override 58 | public Option find(securesocial.core.UserId id) { 59 | UserId javaId = new UserId(); 60 | javaId.id = id.id(); 61 | javaId.provider = id.providerId(); 62 | SocialUser javaUser = doFind(javaId); 63 | securesocial.core.SocialUser scalaUser = null; 64 | if ( javaUser != null ) { 65 | scalaUser = javaUser.toScala(); 66 | } 67 | return Scala.Option(scalaUser); 68 | } 69 | 70 | /** 71 | * Saves the user. This method gets called when a user logs in. 72 | * This is your chance to save the user information in your backing store. 73 | * @param user 74 | */ 75 | @Override 76 | public void save(securesocial.core.SocialUser user) { 77 | doSave(SocialUser.fromScala(user)); 78 | } 79 | 80 | /** 81 | * Saves the user in the backing store 82 | * 83 | * @param user 84 | */ 85 | public abstract void doSave(SocialUser user); 86 | 87 | /** 88 | * Finds the user in the backing store. 89 | */ 90 | public abstract SocialUser doFind(UserId userId); 91 | 92 | } 93 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/GitHubProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers 18 | 19 | import securesocial.core._ 20 | import play.api.{Logger, Application} 21 | import play.api.libs.ws.{Response, WS} 22 | import securesocial.core.UserId 23 | import securesocial.core.SocialUser 24 | import play.api.libs.ws.Response 25 | import securesocial.core.AuthenticationException 26 | import scala.Some 27 | 28 | /** 29 | * A GitHub provider 30 | * 31 | */ 32 | class GitHubProvider(application: Application) extends OAuth2Provider(application) { 33 | val GetAuthenticatedUser = "https://api.github.com/user?access_token=%s" 34 | val AccessToken = "access_token" 35 | val TokenType = "token_type" 36 | val Message = "message" 37 | val Id = "id" 38 | val Name = "name" 39 | val AvatarUrl = "avatar_url" 40 | val Email = "email" 41 | 42 | def providerId = GitHubProvider.GitHub 43 | 44 | override protected def buildInfo(response: Response): OAuth2Info = { 45 | Logger.debug(providerId + " response body: " + response.body) 46 | response.body.split("&|=") match { 47 | case Array(AccessToken, token, TokenType, tokenType) => OAuth2Info(token, Some(tokenType), None) 48 | case _ => 49 | Logger.error("Invalid response format for accessToken") 50 | throw new AuthenticationException() 51 | } 52 | } 53 | 54 | /** 55 | * Subclasses need to implement this method to populate the User object with profile 56 | * information from the service provider. 57 | * 58 | * @param user The user object to be populated 59 | * @return A copy of the user object with the new values set 60 | */ 61 | def fillProfile(user: SocialUser): SocialUser = { 62 | val promise = WS.url(GetAuthenticatedUser.format(user.oAuth2Info.get.accessToken)).get() 63 | promise.await(10000).fold( 64 | error => { 65 | Logger.error( "Error retrieving profile information from github", error) 66 | throw new AuthenticationException() 67 | }, 68 | response => { 69 | val me = response.json 70 | (me \ Message).asOpt[String] match { 71 | case Some(msg) => { 72 | Logger.error("Error retrieving profile information from GitHub. Message = %s".format(msg)) 73 | throw new AuthenticationException() 74 | } 75 | case _ => { 76 | val id = (me \ Id).as[Int] 77 | val displayName = (me \ Name).as[String] 78 | val avatarUrl = (me \ AvatarUrl).asOpt[String] 79 | val email = (me \ Email).asOpt[String].filter( !_.isEmpty ) 80 | user.copy( 81 | id = UserId(id.toString, providerId), 82 | displayName = displayName, 83 | avatarUrl = avatarUrl, 84 | email = email 85 | ) 86 | } 87 | } 88 | 89 | } 90 | ) 91 | } 92 | } 93 | 94 | object GitHubProvider { 95 | val GitHub = "github" 96 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/core/providers/FacebookProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.providers 18 | 19 | import play.api.{Application, Logger} 20 | import play.api.libs.json.{JsString, JsObject} 21 | import securesocial.core._ 22 | import play.api.libs.ws.{Response, WS} 23 | 24 | 25 | /** 26 | * A Facebook Provider 27 | */ 28 | class FacebookProvider(application: Application) extends OAuth2Provider(application) { 29 | val MeApi = "https://graph.facebook.com/me?fields=name,picture,email&access_token=" 30 | val Error = "error" 31 | val Message = "message" 32 | val Type = "type" 33 | val Id = "id" 34 | val Name = "name" 35 | val Picture = "picture" 36 | val Email = "email" 37 | val AccessToken = "access_token" 38 | val Expires = "expires" 39 | val Data = "data" 40 | val Url = "url" 41 | 42 | def providerId = FacebookProvider.Facebook 43 | 44 | // facebook does not follow the OAuth2 spec :-\ 45 | override protected def buildInfo(response: Response): OAuth2Info = { 46 | Logger.debug(providerId + " response body: " + response.body) 47 | response.body.split("&|=") match { 48 | case Array(AccessToken, token, Expires, expiresIn) => OAuth2Info(token, None, Some(expiresIn.toInt)) 49 | case _ => 50 | Logger.error("Invalid response format for accessToken") 51 | throw new AuthenticationException() 52 | } 53 | } 54 | 55 | def fillProfile(user: SocialUser) = { 56 | val accessToken = user.oAuth2Info.get.accessToken 57 | val promise = WS.url(MeApi + accessToken).get() 58 | 59 | promise.await(10000).fold( error => { 60 | Logger.error( "Error retrieving profile information", error) 61 | throw new AuthenticationException() 62 | }, response => { 63 | val me = response.json 64 | (me \ Error).asOpt[JsObject] match { 65 | case Some(error) => 66 | val message = (error \ Message).as[String] 67 | val errorType = ( error \ Type).as[String] 68 | Logger.error("Error retrieving profile information from Facebook. Error type = " + errorType 69 | + ", message: " + message) 70 | throw new AuthenticationException() 71 | case _ => 72 | val id = ( me \ Id).as[String] 73 | val displayName = ( me \ Name).as[String] 74 | val picture = (me \ Picture) 75 | // 76 | // Starting October 2012 the picture field will become a json object. 77 | // making the code compatible with the old and new version for now. 78 | // 79 | val avatarUrl = if ( picture.isInstanceOf[JsString] ) { 80 | picture.asOpt[String] 81 | } else { 82 | (picture \ Data \ Url).asOpt[String] 83 | } 84 | val email = ( me \ Email).as[String] 85 | user.copy( 86 | id = UserId(id.toString, providerId), 87 | displayName = displayName, 88 | avatarUrl = avatarUrl, 89 | email = Some(email) 90 | ) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | object FacebookProvider { 97 | val Facebook = "facebook" 98 | } -------------------------------------------------------------------------------- /module-code/app/securesocial/controllers/LoginPage.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.controllers 18 | 19 | import play.api.mvc.{Action, Controller} 20 | import play.api.i18n.Messages 21 | import securesocial.core._ 22 | import play.api.{Play, Logger} 23 | import Play.current 24 | import play.api.data._ 25 | import play.api.data.Forms._ 26 | import play.api.data.validation.Constraints._ 27 | 28 | 29 | /** 30 | * The Login page controller 31 | */ 32 | object LoginPage extends Controller 33 | { 34 | /** 35 | * The property that specifies the page the user is redirected to if there is no original URL saved in 36 | * the session. 37 | */ 38 | val onLoginGoTo = "securesocial.onLoginGoTo" 39 | 40 | /** 41 | * The property that specifies the page the user is redirected to after logging out. 42 | */ 43 | val onLogoutGoTo = "securesocial.onLogoutGoTo" 44 | 45 | /** 46 | * The root path 47 | */ 48 | val Root = "/" 49 | 50 | 51 | 52 | /** 53 | * Renders the login page 54 | * @return 55 | */ 56 | def login = Action { implicit request => 57 | Ok(securesocial.views.html.login(ProviderRegistry.all().values, securesocial.core.providers.UsernamePasswordProvider.loginForm)) 58 | } 59 | 60 | /** 61 | * Logs out the user by clearing the credentials from the session. 62 | * The browser is redirected either to the login page or to the page specified in the onLogoutGoTo property. 63 | * 64 | * @return 65 | */ 66 | def logout = Action { implicit request => 67 | val to = Play.configuration.getString(onLogoutGoTo).getOrElse(routes.LoginPage.login().absoluteURL()) 68 | Redirect(to).withSession(session - SecureSocial.UserKey - SecureSocial.ProviderKey) 69 | } 70 | 71 | /** 72 | * The authentication flow for all providers starts here. 73 | * 74 | * @param provider The id of the provider that needs to handle the call 75 | * @return 76 | */ 77 | def authenticate(provider: String) = handleAuth(provider) 78 | def authenticateByPost(provider: String) = handleAuth(provider) 79 | 80 | private def handleAuth(provider: String) = Action { implicit request => 81 | ProviderRegistry.get(provider) match { 82 | case Some(p) => { 83 | try { 84 | p.authenticate().fold( result => result , { 85 | user => 86 | if ( Logger.isDebugEnabled ) { 87 | Logger.debug("User logged in : [" + user + "]") 88 | } 89 | val toUrl = session.get(SecureSocial.OriginalUrlKey).getOrElse( 90 | Play.configuration.getString(onLoginGoTo).getOrElse(Root) 91 | ) 92 | Redirect(toUrl).withSession { session + 93 | (SecureSocial.UserKey -> user.id.id) + 94 | (SecureSocial.ProviderKey -> user.id.providerId) - 95 | SecureSocial.OriginalUrlKey 96 | } 97 | }) 98 | } catch { 99 | case ex: AccessDeniedException => Logger.warn("User declined access using provider " + provider) 100 | Redirect(routes.LoginPage.login()).flashing("error" -> Messages("securesocial.login.accessDenied")) 101 | } 102 | } 103 | case _ => NotFound 104 | } 105 | } 106 | 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /module-code/app/securesocial/controllers/Registration.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.controllers 18 | 19 | import play.api.mvc.{Action, Controller} 20 | import play.api.data._ 21 | import play.api.data.Forms._ 22 | import play.api.data.validation.Constraints._ 23 | import play.api.{Play, Logger} 24 | import securesocial.core.providers.UsernamePasswordProvider 25 | import securesocial.core.{AuthenticationMethod, SocialUser, UserId, UserService} 26 | import com.typesafe.plugin._ 27 | import Play.current 28 | import securesocial.core.providers.utils.{GravatarHelper, PasswordHasher} 29 | 30 | 31 | /** 32 | * A controller to handle user registration. 33 | * 34 | */ 35 | object Registration extends Controller { 36 | 37 | val providerId = UsernamePasswordProvider.UsernamePassword 38 | 39 | case class RegistrationInfo(userName: String, fullName: String, email: String, password: String) 40 | 41 | val form = Form[RegistrationInfo]( 42 | mapping( 43 | "userName" -> nonEmptyText.verifying( "Username already taken", userName => { 44 | UserService.find(UserId(userName,providerId)).isEmpty 45 | }), 46 | "fullName" -> nonEmptyText, 47 | ("email" -> 48 | tuple( 49 | "email1" -> email.verifying( nonEmpty ), 50 | "email2" -> email.verifying( nonEmpty ) 51 | ).verifying("Email addresses do not match", emails => emails._1 == emails._2) 52 | ), 53 | ("password" -> 54 | tuple( 55 | "password1" -> nonEmptyText, 56 | "password2" -> nonEmptyText 57 | ).verifying("Passwords do not match", passwords => passwords._1 == passwords._2) 58 | ) 59 | ) 60 | // binding 61 | ((userName, fullName, email, password) => RegistrationInfo(userName, fullName, email._1, password._1)) 62 | // unbinding 63 | (info => Some(info.userName, info.fullName, (info.email, info.email), (info.password, ""))) 64 | ) 65 | 66 | /** 67 | * Renders the sign up page 68 | * @return 69 | */ 70 | def signUp = Action { implicit request => 71 | Ok(securesocial.views.html.Registration.signUp(form)) 72 | } 73 | 74 | /** 75 | * Handles posts from the sign up page 76 | */ 77 | def handleSignUp = Action { implicit request => 78 | form.bindFromRequest.fold ( 79 | errors => { 80 | Logger.info("errors " + errors) 81 | BadRequest(securesocial.views.html.Registration.signUp(errors)) 82 | }, 83 | info => { 84 | Logger.info(info.userName) 85 | val userId = UserId(info.userName,providerId) 86 | val user = SocialUser(userId, info.fullName, 87 | Some(info.email), 88 | GravatarHelper.avatarFor(info.email), 89 | AuthenticationMethod.UserPassword, 90 | passwordInfo = Some(use[PasswordHasher].hash(info.password))) 91 | UserService.save(user) 92 | Redirect(routes.LoginPage.login()).flashing("success" -> "Thank you for signing up. Check your email for further instructions") 93 | } 94 | ) 95 | } 96 | 97 | /** 98 | * The action invoked from the activation email the user receives after signing up 99 | * @return 100 | */ 101 | def activateAccount = Action { implicit request => 102 | Ok("") 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/IdentityProvider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import securesocial.controllers.routes 20 | import play.api.mvc.{Request, Result} 21 | import play.api.{Application, Logger, Plugin} 22 | 23 | /** 24 | * Base class for all Identity Providers. All providers are plugins and are loaded 25 | * automatically at application start time. 26 | * 27 | * 28 | */ 29 | abstract class IdentityProvider(application: Application) extends Plugin { 30 | val SecureSocial = "securesocial." 31 | val Dot = "." 32 | val CallbackUrl = "callbackBaseUrl" 33 | 34 | 35 | /** 36 | * Registers the provider in the Provider Registry 37 | */ 38 | override def onStart() { 39 | ProviderRegistry.register(this) 40 | } 41 | 42 | /** 43 | * Subclasses need to implement this to specify the provider type 44 | * @return 45 | */ 46 | def providerId: String 47 | 48 | /** 49 | * Subclasses need to implement this to specify the authentication method 50 | * @return 51 | */ 52 | def authMethod: AuthenticationMethod 53 | 54 | /** 55 | * Returns the provider name 56 | * 57 | * @return 58 | */ 59 | override def toString = providerId 60 | 61 | /** 62 | * Authenticates the user and fills the profile information. Returns either a User if all went 63 | * ok or a Result that the controller sents to the browser (eg: in the case of OAuth for example 64 | * where the user needs to be redirected to the service provider) 65 | * 66 | * @param request 67 | * @tparam A 68 | * @return 69 | */ 70 | def authenticate[A]()(implicit request: Request[A]):Either[Result, SocialUser] = { 71 | doAuth().fold( 72 | result => Left(result), 73 | u => 74 | { 75 | val user = fillProfile(u) 76 | UserService.save(user) 77 | Right(user) 78 | } 79 | ) 80 | } 81 | 82 | /** 83 | * The url for this provider. This is used in the login page template to point each icon 84 | * to the provider url. 85 | * @return 86 | */ 87 | def authenticationUrl:String = routes.LoginPage.authenticate(providerId).url 88 | 89 | /** 90 | * The callback url the provider uses to redirect client back to our application. 91 | * @return String 92 | */ 93 | def getCallbackUrl[A](implicit request: Request[A]): String = { 94 | application.configuration.getString(SecureSocial + CallbackUrl) match { 95 | case None => routes.LoginPage.authenticate(providerId).absoluteURL() 96 | case Some(x) => x + routes.LoginPage.authenticate(providerId).url 97 | } 98 | } 99 | 100 | /** 101 | * The property key used for all the provider properties. 102 | * 103 | * @return 104 | */ 105 | def propertyKey = SecureSocial + providerId + Dot 106 | 107 | /** 108 | * Reads a property from the application.conf 109 | * @param property 110 | * @return 111 | */ 112 | def loadProperty(property: String): Option[String] = { 113 | val result = application.configuration.getString(propertyKey + property) 114 | if ( !result.isDefined ) { 115 | Logger.error("[securesocial] Missing property " + property + " for provider " + providerId) 116 | } 117 | result 118 | } 119 | 120 | 121 | /** 122 | * Subclasses need to implement the authentication logic. This method needs to return 123 | * a User object that then gets passed to the fillProfile method 124 | * 125 | * @param request 126 | * @tparam A 127 | * @return Either a Result or a User 128 | */ 129 | def doAuth[A]()(implicit request: Request[A]):Either[Result, SocialUser] 130 | 131 | /** 132 | * Subclasses need to implement this method to populate the User object with profile 133 | * information from the service provider. 134 | * 135 | * @param user The user object to be populated 136 | * @return A copy of the user object with the new values set 137 | */ 138 | def fillProfile(user: SocialUser):SocialUser 139 | } 140 | 141 | object IdentityProvider { 142 | val SessionId = "securesocial.id" 143 | } 144 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/OAuth1Provider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import _root_.java.util.UUID 20 | import play.api.cache.Cache 21 | import play.api.libs.oauth.{RequestToken, ConsumerKey, OAuth, ServiceInfo} 22 | import play.api.{Application, Logger, Play} 23 | import securesocial.controllers.routes 24 | import play.api.mvc.{Request, Result} 25 | import play.api.mvc.Results.Redirect 26 | import Play.current 27 | 28 | 29 | /** 30 | * Base class for all OAuth1 providers 31 | */ 32 | abstract class OAuth1Provider(application: Application) extends IdentityProvider(application) { 33 | val serviceInfo = createServiceInfo(propertyKey) 34 | val service = OAuth(serviceInfo, true) 35 | 36 | def authMethod = AuthenticationMethod.OAuth1 37 | 38 | def createServiceInfo(key: String): ServiceInfo = { 39 | val result = for { 40 | requestTokenUrl <- loadProperty(OAuth1Provider.RequestTokenUrl) ; 41 | accessTokenUrl <- loadProperty(OAuth1Provider.AccessTokenUrl) ; 42 | authorizationUrl <- loadProperty(OAuth1Provider.AuthorizationUrl) ; 43 | consumerKey <- loadProperty(OAuth1Provider.ConsumerKey) ; 44 | consumerSecret <- loadProperty(OAuth1Provider.ConsumerSecret) 45 | } yield { 46 | ServiceInfo(requestTokenUrl, accessTokenUrl, authorizationUrl, ConsumerKey(consumerKey, consumerSecret)) 47 | } 48 | 49 | if ( result.isEmpty ) { 50 | throw new RuntimeException("Missing properties for provider " + providerId) 51 | } 52 | result.get 53 | } 54 | 55 | 56 | def doAuth[A]()(implicit request: Request[A]):Either[Result, SocialUser] = { 57 | if ( request.queryString.get("denied").isDefined ) { 58 | // the user did not grant access to the account 59 | throw new AccessDeniedException() 60 | } 61 | 62 | request.queryString.get("oauth_verifier").map { seq => 63 | val verifier = seq.head 64 | // 2nd step in the oauth flow, we have the access token in the cache, we need to 65 | // swap it for the access token 66 | val user = for { 67 | cacheKey <- request.session.get("cacheKey") 68 | requestToken <- Cache.getAs[RequestToken](cacheKey) 69 | } yield { 70 | service.retrieveAccessToken(RequestToken(requestToken.token, requestToken.secret), verifier) match { 71 | case Right(token) => 72 | // the Cache api does not have a remove method. Just set the cache key and expire it after 1 second for 73 | // now. 74 | Cache.set(cacheKey, Unit, 1) 75 | Right( 76 | SocialUser( 77 | UserId("", providerId), "", None, None, authMethod, 78 | oAuth1Info = Some(OAuth1Info(serviceInfo, token.token, token.secret)) 79 | ) 80 | ) 81 | case Left(oauthException) => 82 | Logger.error("Error retrieving access token", oauthException) 83 | throw new AuthenticationException() 84 | } 85 | } 86 | user.getOrElse( throw new AuthenticationException() ) 87 | }.getOrElse { 88 | // the oauth_verifier field is not in the request, this is the 1st step in the auth flow. 89 | // we need to get the request tokens 90 | val callbackUrl = getCallbackUrl 91 | if ( Logger.isDebugEnabled ) { 92 | Logger.debug("callback url = " + callbackUrl) 93 | } 94 | service.retrieveRequestToken(callbackUrl) match { 95 | case Right(accessToken) => 96 | val cacheKey = UUID.randomUUID().toString 97 | val redirect = Redirect(service.redirectUrl(accessToken.token)).withSession("cacheKey" -> cacheKey) 98 | Cache.set(cacheKey, accessToken, 600) // set it for 10 minutes, plenty of time to log in 99 | Left(redirect) 100 | case Left(e) => 101 | Logger.error("Error retrieving request token", e) 102 | throw new AuthenticationException() 103 | } 104 | } 105 | } 106 | } 107 | 108 | object OAuth1Provider { 109 | val RequestTokenUrl = "requestTokenUrl" 110 | val AccessTokenUrl = "accessTokenUrl" 111 | val AuthorizationUrl = "authorizationUrl" 112 | val ConsumerKey = "consumerKey" 113 | val ConsumerSecret = "consumerSecret" 114 | } 115 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/SocialUser.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | import play.libs.Scala; 20 | import scala.Option; 21 | import securesocial.core.PasswordInfo$; 22 | 23 | /** 24 | * A class representing a connected user and its authentication details. 25 | */ 26 | public class SocialUser { 27 | /** 28 | * The user id 29 | */ 30 | public UserId id; 31 | 32 | /** 33 | * The user display name. 34 | */ 35 | public String displayName; 36 | 37 | /** 38 | * The user's email (some providers can't provide the email, eg: twitter) 39 | */ 40 | public String email; 41 | 42 | /** 43 | * A URL pointing to the user avatar 44 | */ 45 | public String avatarUrl; 46 | 47 | /** 48 | * The method that was used to authenticate the user. 49 | */ 50 | public AuthenticationMethod authMethod; 51 | 52 | /** 53 | * 54 | */ 55 | public boolean isEmailVerified; 56 | 57 | /** 58 | * The OAuth1 details required to make calls to the API for OAUTH1 users 59 | * (available when authMethod is OAUTH1) 60 | * 61 | */ 62 | public OAuth1Info oAuth1Info; 63 | 64 | /** 65 | * The OAuth2 details required to make calls to the API for OAUTH2 users 66 | * (available when authMethod is OAUTH2) 67 | * 68 | */ 69 | public OAuth2Info oAuth2Info; 70 | 71 | /** 72 | * The Password and the salt used to hash it (available when authMethod is USERNAME_PASSWORD) 73 | */ 74 | public PasswordInfo passwordInfo; 75 | 76 | public static SocialUser fromScala(securesocial.core.SocialUser scalaUser) { 77 | SocialUser user = new SocialUser(); 78 | user.id = new UserId(); 79 | user.id.id = scalaUser.id().id(); 80 | user.id.provider = scalaUser.id().providerId(); 81 | user.displayName = scalaUser.displayName(); 82 | user.avatarUrl = Scala.orNull(scalaUser.avatarUrl()); 83 | user.email = Scala.orNull(scalaUser.email()); 84 | user.authMethod = AuthenticationMethod.fromScala(scalaUser.authMethod()); 85 | user.isEmailVerified = scalaUser.isEmailVerified(); 86 | 87 | if ( scalaUser.oAuth1Info().isDefined() ) { 88 | user.oAuth1Info = OAuth1Info.fromScala(scalaUser.oAuth1Info().get()); 89 | } 90 | 91 | if ( scalaUser.oAuth2Info().isDefined() ) { 92 | user.oAuth2Info = OAuth2Info.fromScala(scalaUser.oAuth2Info().get()); 93 | } 94 | 95 | if ( scalaUser.passwordInfo().isDefined() ) { 96 | user.passwordInfo = PasswordInfo.fromScala(scalaUser.passwordInfo().get()); 97 | } 98 | return user; 99 | } 100 | 101 | public securesocial.core.SocialUser toScala() { 102 | securesocial.core.UserId userId = securesocial.core.UserId$.MODULE$.apply(id.id, id.provider); 103 | return securesocial.core.SocialUser$.MODULE$.apply(userId, 104 | displayName, 105 | Scala.Option(email), 106 | Scala.Option(avatarUrl), 107 | AuthenticationMethod.toSala(authMethod), 108 | isEmailVerified, 109 | optionalOAuth1Info(), 110 | optionalOAuth2Info(), 111 | optionalPasswordInfo() 112 | ); 113 | } 114 | 115 | private Option optionalOAuth1Info() { 116 | securesocial.core.OAuth1Info scalaInfo = null; 117 | 118 | if ( oAuth1Info != null ) { 119 | // serviceInfo does not need conversion because it's a Scala object already. 120 | scalaInfo = securesocial.core.OAuth1Info$.MODULE$.apply(oAuth1Info.serviceInfo, oAuth1Info.token, oAuth1Info.secret); 121 | } 122 | return Scala.Option(scalaInfo); 123 | } 124 | 125 | private Option optionalOAuth2Info() { 126 | securesocial.core.OAuth2Info scalaInfo = null; 127 | 128 | if ( oAuth2Info != null ) { 129 | scalaInfo = securesocial.core.OAuth2Info$.MODULE$.apply( 130 | oAuth2Info.accessToken, 131 | Scala.Option(oAuth2Info.tokenType), 132 | Scala.Option((Object)oAuth2Info.expiresIn), 133 | Scala.Option(oAuth2Info.refreshToken) 134 | ); 135 | } 136 | return Scala.Option(scalaInfo); 137 | } 138 | 139 | private Option optionalPasswordInfo() { 140 | securesocial.core.PasswordInfo scalaInfo = null; 141 | if ( passwordInfo != null ) { 142 | scalaInfo = securesocial.core.PasswordInfo$.MODULE$.apply(passwordInfo.password, Scala.Option(passwordInfo.salt)); 143 | } 144 | return Scala.Option(scalaInfo); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/SecureSocial.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import play.api.mvc._ 20 | import securesocial.controllers.routes 21 | import play.api.i18n.Messages 22 | import play.api.Logger 23 | import play.api.libs.json.Json 24 | 25 | 26 | /** 27 | * Provides the actions that can be used to protect controllers and retrieve the current user 28 | * if available. 29 | * 30 | * object MyController extends SecureSocial { 31 | * def protectedAction = SecuredAction() { implicit request => 32 | * Ok("Hello %s".format(request.user.displayName)) 33 | * } 34 | */ 35 | trait SecureSocial extends Controller { 36 | 37 | /** 38 | * A request that adds the User for the current call 39 | */ 40 | case class SecuredRequest[A](user: SocialUser, request: Request[A]) extends WrappedRequest(request) 41 | 42 | /** 43 | * A Forbidden response for API clients 44 | * @param request 45 | * @tparam A 46 | * @return 47 | */ 48 | private def apiClientForbidden[A](implicit request: Request[A]): Result = { 49 | Forbidden(Json.toJson(Map("error"->"Credentials required"))).withSession { 50 | session - SecureSocial.UserKey - SecureSocial.ProviderKey 51 | }.as(JSON) 52 | } 53 | 54 | /** 55 | * A secured action. If there is no user in the session the request is redirected 56 | * to the login page 57 | * 58 | * @param apiClient A boolean specifying if this is request is for an API or not 59 | * @param p 60 | * @param f 61 | * @tparam A 62 | * @return 63 | */ 64 | def SecuredAction[A](apiClient: Boolean, p: BodyParser[A])(f: SecuredRequest[A] => Result) = Action(p) { 65 | implicit request => { 66 | SecureSocial.userFromSession(request).map { userId => 67 | UserService.find(userId).map { user => 68 | f(SecuredRequest(user, request)) 69 | }.getOrElse { 70 | // there is no user in the backing store matching the credentials sent by the client. 71 | // we need to remove the credentials from the session 72 | if ( apiClient ) { 73 | apiClientForbidden(request) 74 | } else { 75 | Redirect(routes.LoginPage.logout()) 76 | } 77 | } 78 | }.getOrElse { 79 | if ( Logger.isDebugEnabled ) { 80 | Logger.debug("Anonymous user trying to access : '%s'".format(request.uri)) 81 | } 82 | if ( apiClient ) { 83 | apiClientForbidden(request) 84 | } else { 85 | Redirect(routes.LoginPage.login()).flashing("error" -> Messages("securesocial.loginRequired")).withSession( 86 | session + (SecureSocial.OriginalUrlKey -> request.uri) 87 | ) 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * A secured action. If there is no user in the session the request is redirected 95 | * to the login page. 96 | * @param f 97 | * @return 98 | */ 99 | def SecuredAction(apiClient: Boolean = false)(f: SecuredRequest[AnyContent] => Result): Action[AnyContent] = { 100 | SecuredAction(apiClient, parse.anyContent)(f) 101 | } 102 | 103 | /** 104 | * A request that adds the User for the current call 105 | */ 106 | case class RequestWithUser[A](user: Option[SocialUser], request: Request[A]) extends WrappedRequest(request) 107 | 108 | /** 109 | * An action that adds the current user in the request if it's available 110 | * 111 | * @param p 112 | * @param f 113 | * @tparam A 114 | * @return 115 | */ 116 | def UserAwareAction[A](p: BodyParser[A])(f: RequestWithUser[A] => Result) = Action(p) { 117 | implicit request => 118 | f(RequestWithUser(SecureSocial.currentUser, request)) 119 | } 120 | 121 | /** 122 | * An action that adds the current user in the request if it's available 123 | * @param f 124 | * @return 125 | */ 126 | def UserAwareAction(f: RequestWithUser[AnyContent] => Result): Action[AnyContent] = { 127 | UserAwareAction(parse.anyContent)(f) 128 | } 129 | } 130 | 131 | object SecureSocial { 132 | val UserKey = "securesocial.user" 133 | val ProviderKey = "securesocial.provider" 134 | val OriginalUrlKey = "securesocial.originalUrl" 135 | 136 | /** 137 | * Build a UserId object from the session data 138 | * 139 | * @param request 140 | * @tparam A 141 | * @return 142 | */ 143 | def userFromSession[A](implicit request: Request[A]):Option[UserId] = { 144 | for ( 145 | userId <- request.session.get(SecureSocial.UserKey); 146 | providerId <- request.session.get(SecureSocial.ProviderKey) 147 | ) yield { 148 | UserId(userId, providerId) 149 | } 150 | } 151 | 152 | /** 153 | * Get the current logged in user. This method can be used from public actions that need to 154 | * access the current user if there's any 155 | * 156 | * @param request 157 | * @tparam A 158 | * @return 159 | */ 160 | def currentUser[A](implicit request: Request[A]):Option[SocialUser] = { 161 | for ( 162 | userId <- userFromSession ; 163 | user <- UserService.find(userId) 164 | ) yield { 165 | fillServiceInfo(user) 166 | } 167 | } 168 | 169 | def fillServiceInfo(user: SocialUser): SocialUser = { 170 | if ( user.authMethod == AuthenticationMethod.OAuth1 ) { 171 | // if the user is using OAuth1 make sure we're also returning 172 | // the right service info 173 | ProviderRegistry.get(user.id.providerId).map { p => 174 | val si = p.asInstanceOf[OAuth1Provider].serviceInfo 175 | val oauthInfo = user.oAuth1Info.get.copy(serviceInfo = si) 176 | user.copy( oAuth1Info = Some(oauthInfo)) 177 | }.get 178 | } else { 179 | user 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/OAuth2Provider.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core 18 | 19 | import _root_.java.net.URLEncoder 20 | import _root_.java.util.UUID 21 | import play.api.{Logger, Play, Application} 22 | import play.api.cache.Cache 23 | import Play.current 24 | import play.api.mvc.{Results, Result, Request} 25 | import securesocial.controllers.routes 26 | import play.api.libs.ws.{Response, WS} 27 | 28 | /** 29 | * Base class for all OAuth2 providers 30 | */ 31 | abstract class OAuth2Provider(application: Application) extends IdentityProvider(application) { 32 | val settings = createSettings() 33 | 34 | def authMethod = AuthenticationMethod.OAuth2 35 | 36 | private def createSettings(): OAuth2Settings = { 37 | val result = for { 38 | authorizationUrl <- loadProperty(OAuth2Settings.AuthorizationUrl) ; 39 | accessToken <- loadProperty(OAuth2Settings.AccessTokenUrl) ; 40 | clientId <- loadProperty(OAuth2Settings.ClientId) ; 41 | clientSecret <- loadProperty(OAuth2Settings.ClientSecret) 42 | } yield { 43 | val scope = application.configuration.getString(propertyKey + OAuth2Settings.Scope) 44 | OAuth2Settings(authorizationUrl, accessToken, clientId, clientSecret, scope) 45 | } 46 | if ( !result.isDefined ) { 47 | throw new RuntimeException("Missing properties for provider " + providerId) 48 | } 49 | result.get 50 | } 51 | 52 | private def getAccessToken[A](code: String)(implicit request: Request[A]):OAuth2Info = { 53 | val params = Map( 54 | OAuth2Constants.ClientId -> Seq(settings.clientId), 55 | OAuth2Constants.ClientSecret -> Seq(settings.clientSecret), 56 | OAuth2Constants.GrantType -> Seq(OAuth2Constants.AuthorizationCode), 57 | OAuth2Constants.Code -> Seq(code), 58 | OAuth2Constants.RedirectUri -> Seq(getCallbackUrl) 59 | ) 60 | WS.url(settings.accessTokenUrl).post(params).await(10000).fold( onError => 61 | { 62 | Logger.error("Timed out trying to get an access token for provider " + providerId) 63 | throw new AuthenticationException() 64 | }, 65 | response => buildInfo(response) 66 | ) 67 | } 68 | 69 | protected def buildInfo(response: Response): OAuth2Info = { 70 | Logger.debug(providerId + " response body: " + response.body) 71 | val json = response.json 72 | Logger.debug("Got json back [" + json + "]") 73 | OAuth2Info( 74 | (json \ OAuth2Constants.AccessToken).as[String], 75 | (json \ OAuth2Constants.TokenType).asOpt[String], 76 | (json \ OAuth2Constants.ExpiresIn).asOpt[Int], 77 | (json \ OAuth2Constants.RefreshToken).asOpt[String] 78 | ) 79 | } 80 | 81 | def doAuth[A]()(implicit request: Request[A]): Either[Result, SocialUser] = { 82 | request.queryString.get(OAuth2Constants.Error).flatMap(_.headOption).map( error => { 83 | Logger.error(providerId + " error = [" + error + "]") 84 | error match { 85 | case OAuth2Constants.AccessDenied => throw new AccessDeniedException() 86 | case _ => 87 | Logger.error("Error '" + error + "' returned by the authorization server. Provider type is " + providerId) 88 | throw new AuthenticationException() 89 | } 90 | throw new AuthenticationException() 91 | }) 92 | 93 | request.queryString.get(OAuth2Constants.Code).flatMap(_.headOption) match { 94 | case Some(code) => 95 | // we're being redirected back from the authorization server with the access code. 96 | val user = for ( 97 | // check if the state we sent is equal to the one we're receiving now before continuing the flow. 98 | sessionId <- request.session.get(IdentityProvider.SessionId) ; 99 | originalState <- Cache.getAs[String](sessionId) ; 100 | currentState <- request.queryString.get(OAuth2Constants.State).flatMap(_.headOption) if originalState == currentState 101 | ) yield { 102 | val accessToken = getAccessToken(code) 103 | val oauth2Info = Some( 104 | OAuth2Info(accessToken.accessToken, accessToken.tokenType, accessToken.expiresIn, accessToken.refreshToken) 105 | ) 106 | SocialUser(UserId("", providerId), "", None, None, authMethod, oAuth2Info = oauth2Info) 107 | } 108 | if ( Logger.isDebugEnabled ) { 109 | Logger.debug("user = " + user) 110 | } 111 | user match { 112 | case Some(u) => Right(u) 113 | case _ => throw new AuthenticationException() 114 | } 115 | case None => 116 | // There's no code in the request, this is the first step in the oauth flow 117 | val state = UUID.randomUUID().toString 118 | val sessionId = request.session.get(IdentityProvider.SessionId).getOrElse(UUID.randomUUID().toString) 119 | Cache.set(sessionId, state) 120 | var params = List( 121 | (OAuth2Constants.ClientId, settings.clientId), 122 | (OAuth2Constants.RedirectUri, getCallbackUrl), 123 | (OAuth2Constants.ResponseType, OAuth2Constants.Code), 124 | (OAuth2Constants.State, state)) 125 | settings.scope.foreach( s => { params = (OAuth2Constants.Scope, s) :: params }) 126 | val url = settings.authorizationUrl + 127 | params.map( p => p._1 + "=" + URLEncoder.encode(p._2, "UTF-8")).mkString("?", "&", "") 128 | if ( Logger.isDebugEnabled ) { 129 | Logger.debug("params : " + params) 130 | Logger.debug("authorizationUrl = " + settings.authorizationUrl) 131 | Logger.debug("Redirecting to : [" + url + "]") 132 | } 133 | Left(Results.Redirect( url ).withSession(request.session + (IdentityProvider.SessionId, sessionId))) 134 | } 135 | } 136 | } 137 | 138 | case class OAuth2Settings(authorizationUrl: String, accessTokenUrl: String, clientId: String, 139 | clientSecret: String, scope: Option[String] 140 | ) 141 | 142 | object OAuth2Settings { 143 | val AuthorizationUrl = "authorizationUrl" 144 | val AccessTokenUrl = "accessTokenUrl" 145 | val ClientId = "clientId" 146 | val ClientSecret = "clientSecret" 147 | val Scope = "scope" 148 | } 149 | 150 | object OAuth2Constants { 151 | val ClientId = "client_id" 152 | val ClientSecret = "client_secret" 153 | val RedirectUri = "redirect_uri" 154 | val Scope = "scope" 155 | val ResponseType = "response_type" 156 | val State = "state" 157 | val GrantType = "grant_type" 158 | val AuthorizationCode = "authorization_code" 159 | val AccessToken = "access_token" 160 | val Error = "error" 161 | val Code = "code" 162 | val TokenType = "token_type" 163 | val ExpiresIn = "expires_in" 164 | val RefreshToken = "refresh_token" 165 | val AccessDenied = "access_denied" 166 | } 167 | -------------------------------------------------------------------------------- /module-code/app/securesocial/core/java/SecureSocial.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package securesocial.core.java; 18 | 19 | import org.codehaus.jackson.node.ObjectNode; 20 | import play.Logger; 21 | import play.libs.Json; 22 | import play.mvc.Action; 23 | import play.mvc.Http; 24 | import play.mvc.Result; 25 | import play.mvc.With; 26 | import scala.Option; 27 | 28 | import java.lang.annotation.ElementType; 29 | import java.lang.annotation.Retention; 30 | import java.lang.annotation.RetentionPolicy; 31 | import java.lang.annotation.Target; 32 | 33 | /** 34 | * Provides the actions that can be used to protect controllers and retrieve the current user 35 | * if available. 36 | * 37 | * Sample usage: 38 | * 39 | * @SecureSocial.Secured 40 | * public static Result index() { 41 | * SocialUser user = (SocialUser) ctx().args.get(SecureSocial.USER_KEY); 42 | * return ok("Hello " + user.displayName); 43 | * } 44 | */ 45 | public class SecureSocial { 46 | 47 | /** 48 | * The user key 49 | */ 50 | public static final String USER_KEY = "securesocial.user"; 51 | 52 | /** 53 | * The provider key 54 | */ 55 | static final String PROVIDER_KEY = "securesocial.provider"; 56 | 57 | /** 58 | * The original url key 59 | */ 60 | static final String ORIGINAL_URL = "securesocial.originalUrl"; 61 | 62 | /** 63 | * An annotation to mark actions as protected by SecureSocial 64 | * When the user is not logged in the action redirects the browser to the login page. 65 | * 66 | * If the isApiClient parameter is set to true SecureSocial will return a forbidden error 67 | * with a json error instead. 68 | */ 69 | @With(SecuredAction.class) 70 | @Target({ElementType.TYPE, ElementType.METHOD}) 71 | @Retention(RetentionPolicy.RUNTIME) 72 | public @interface Secured { 73 | /** 74 | * Specifies wether the action handles an API call or not. Default is false. 75 | * @return 76 | */ 77 | boolean isApiClient() default false; 78 | } 79 | 80 | /** 81 | * Creates a UserId from the session 82 | * 83 | * @param ctx the current context 84 | * @return the UserId or null if there's no user in the session 85 | */ 86 | private static securesocial.core.UserId getUserIdFromSession(Http.Context ctx) { 87 | final String user = ctx.session().get(USER_KEY); 88 | final String provider = ctx.session().get(PROVIDER_KEY); 89 | securesocial.core.UserId result = null; 90 | 91 | if ( user != null && provider != null ) { 92 | result = new securesocial.core.UserId( 93 | user, 94 | provider 95 | ); 96 | } 97 | return result; 98 | } 99 | 100 | /** 101 | * Returns the current user 102 | * 103 | * @return a SocialUser or null if there is no current user 104 | */ 105 | public static SocialUser currentUser() { 106 | SocialUser result = null; 107 | securesocial.core.UserId scalaUserId = getUserIdFromSession(Http.Context.current()); 108 | 109 | if ( scalaUserId != null ) { 110 | Option option = securesocial.core.UserService$.MODULE$.find(scalaUserId); 111 | if ( option.isDefined() ) { 112 | securesocial.core.SocialUser scalaUser = securesocial.core.SecureSocial$.MODULE$.fillServiceInfo(option.get()); 113 | result = SocialUser.fromScala(scalaUser); 114 | } 115 | } 116 | return result; 117 | } 118 | 119 | /** 120 | * Generates the json required for API calls. 121 | * 122 | * @return 123 | */ 124 | private static ObjectNode forbiddenJson() { 125 | ObjectNode result = Json.newObject(); 126 | result.put("error", "Credentials required"); 127 | return result; 128 | } 129 | 130 | private static void fixHttpContext(Http.Context ctx) { 131 | // As of Play 2.0.3: 132 | // I don't understand why the ctx is not set in the Http.Context thread local variable. 133 | // I'm setting it by hand so I can retrieve the i18n messages and currentUser() can work. 134 | // will find out later why this is working this way, if you know why this is not set let me know :) 135 | // This is looks like a bug, Play should be setting the context properly. 136 | Http.Context.current.set(ctx); 137 | } 138 | 139 | /** 140 | * Protects an action with SecureSocial 141 | */ 142 | public static class SecuredAction extends Action { 143 | 144 | @Override 145 | public Result call(Http.Context ctx) throws Throwable { 146 | try { 147 | fixHttpContext(ctx); 148 | securesocial.core.UserId scalaUserId = getUserIdFromSession(ctx); 149 | 150 | if ( scalaUserId == null ) { 151 | if ( Logger.isDebugEnabled() ) { 152 | Logger.debug("Anonymous user trying to access : " + ctx.request().uri()); 153 | } 154 | if ( configuration.isApiClient() ) { 155 | return forbidden( forbiddenJson() ); 156 | } else { 157 | ctx.flash().put("error", play.i18n.Messages.get("securesocial.loginRequired")); 158 | ctx.session().put(ORIGINAL_URL, ctx.request().uri()); 159 | return redirect(securesocial.controllers.routes.LoginPage.login()); 160 | } 161 | } else { 162 | SocialUser user = currentUser(); 163 | if ( user != null ) { 164 | ctx.args.put(USER_KEY, user); 165 | return delegate.call(ctx); 166 | } else { 167 | // there is no user in the backing store matching the credentials sent by the client. 168 | // we need to remove the credentials from the session 169 | if ( configuration.isApiClient() ) { 170 | ctx.session().remove(USER_KEY); 171 | ctx.session().remove(PROVIDER_KEY); 172 | return forbidden( forbiddenJson() ); 173 | } else { 174 | return redirect(securesocial.controllers.routes.LoginPage.logout()); 175 | } 176 | } 177 | } 178 | } finally { 179 | // leave it null as it was before, just in case. 180 | Http.Context.current.set(null); 181 | } 182 | } 183 | } 184 | 185 | 186 | /** 187 | * Actions annotated with UserAwareAction get the current user set in the Context.args holder 188 | * if there's one available. 189 | */ 190 | @With(UserAwareAction.class) 191 | @Target({ElementType.TYPE, ElementType.METHOD}) 192 | @Retention(RetentionPolicy.RUNTIME) 193 | public @interface UserAware { 194 | } 195 | 196 | /** 197 | * An action that puts the current user in the context if there's one available. This is useful in 198 | * public actions that need to access the user information if there's one logged in. 199 | */ 200 | public static class UserAwareAction extends Action { 201 | @Override 202 | public Result call(Http.Context ctx) throws Throwable { 203 | SecureSocial.fixHttpContext(ctx); 204 | try { 205 | SocialUser user = currentUser(); 206 | if ( user != null ) { 207 | ctx.args.put(USER_KEY, user); 208 | } 209 | return delegate.call(ctx); 210 | } finally { 211 | // leave it null as it was before, just in case. 212 | Http.Context.current.set(null); 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /module-code/public/bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.3 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}@media(max-width:767px){.visible-phone{display:inherit!important}.hidden-phone{display:none!important}.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}}@media(min-width:768px) and (max-width:979px){.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:18px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{position:absolute;top:10px;right:10px;left:10px;width:auto;margin:0}.modal.fade.in{top:auto}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:auto;margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:20px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.762430939%;*margin-left:2.709239449638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:99.999999993%;*width:99.9468085036383%}.row-fluid .span11{width:91.436464082%;*width:91.38327259263829%}.row-fluid .span10{width:82.87292817100001%;*width:82.8197366816383%}.row-fluid .span9{width:74.30939226%;*width:74.25620077063829%}.row-fluid .span8{width:65.74585634900001%;*width:65.6926648596383%}.row-fluid .span7{width:57.182320438000005%;*width:57.129128948638304%}.row-fluid .span6{width:48.618784527%;*width:48.5655930376383%}.row-fluid .span5{width:40.055248616%;*width:40.0020571266383%}.row-fluid .span4{width:31.491712705%;*width:31.4385212156383%}.row-fluid .span3{width:22.928176794%;*width:22.874985304638297%}.row-fluid .span2{width:14.364640883%;*width:14.311449393638298%}.row-fluid .span1{width:5.801104972%;*width:5.747913482638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:714px}input.span11,textarea.span11,.uneditable-input.span11{width:652px}input.span10,textarea.span10,.uneditable-input.span10{width:590px}input.span9,textarea.span9,.uneditable-input.span9{width:528px}input.span8,textarea.span8,.uneditable-input.span8{width:466px}input.span7,textarea.span7,.uneditable-input.span7{width:404px}input.span6,textarea.span6,.uneditable-input.span6{width:342px}input.span5,textarea.span5,.uneditable-input.span5{width:280px}input.span4,textarea.span4,.uneditable-input.span4{width:218px}input.span3,textarea.span3,.uneditable-input.span3{width:156px}input.span2,textarea.span2,.uneditable-input.span2{width:94px}input.span1,textarea.span1,.uneditable-input.span1{width:32px}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:30px}.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.564102564%;*margin-left:2.510911074638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145300001%;*width:91.3997999636383%}.row-fluid .span10{width:82.905982906%;*width:82.8527914166383%}.row-fluid .span9{width:74.358974359%;*width:74.30578286963829%}.row-fluid .span8{width:65.81196581200001%;*width:65.7587743226383%}.row-fluid .span7{width:57.264957265%;*width:57.2117657756383%}.row-fluid .span6{width:48.717948718%;*width:48.6647572286383%}.row-fluid .span5{width:40.170940171000005%;*width:40.117748681638304%}.row-fluid .span4{width:31.623931624%;*width:31.5707401346383%}.row-fluid .span3{width:23.076923077%;*width:23.0237315876383%}.row-fluid .span2{width:14.529914530000001%;*width:14.4767230406383%}.row-fluid .span1{width:5.982905983%;*width:5.929714493638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:1160px}input.span11,textarea.span11,.uneditable-input.span11{width:1060px}input.span10,textarea.span10,.uneditable-input.span10{width:960px}input.span9,textarea.span9,.uneditable-input.span9{width:860px}input.span8,textarea.span8,.uneditable-input.span8{width:760px}input.span7,textarea.span7,.uneditable-input.span7{width:660px}input.span6,textarea.span6,.uneditable-input.span6{width:560px}input.span5,textarea.span5,.uneditable-input.span5{width:460px}input.span4,textarea.span4,.uneditable-input.span4{width:360px}input.span3,textarea.span3,.uneditable-input.span3{width:260px}input.span2,textarea.span2,.uneditable-input.span2{width:160px}input.span1,textarea.span1,.uneditable-input.span1{width:60px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top{position:static;margin-bottom:18px}.navbar-fixed-top .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 9px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#999;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#222}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222;border-bottom:1px solid #222;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /module-code/public/bootstrap/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.3 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | content: ""; 19 | } 20 | 21 | .clearfix:after { 22 | clear: both; 23 | } 24 | 25 | .hide-text { 26 | font: 0/0 a; 27 | color: transparent; 28 | text-shadow: none; 29 | background-color: transparent; 30 | border: 0; 31 | } 32 | 33 | .input-block-level { 34 | display: block; 35 | width: 100%; 36 | min-height: 28px; 37 | -webkit-box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | -ms-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | @media (max-width: 767px) { 61 | .visible-phone { 62 | display: inherit !important; 63 | } 64 | .hidden-phone { 65 | display: none !important; 66 | } 67 | .hidden-desktop { 68 | display: inherit !important; 69 | } 70 | .visible-desktop { 71 | display: none !important; 72 | } 73 | } 74 | 75 | @media (min-width: 768px) and (max-width: 979px) { 76 | .visible-tablet { 77 | display: inherit !important; 78 | } 79 | .hidden-tablet { 80 | display: none !important; 81 | } 82 | .hidden-desktop { 83 | display: inherit !important; 84 | } 85 | .visible-desktop { 86 | display: none !important ; 87 | } 88 | } 89 | 90 | @media (max-width: 480px) { 91 | .nav-collapse { 92 | -webkit-transform: translate3d(0, 0, 0); 93 | } 94 | .page-header h1 small { 95 | display: block; 96 | line-height: 18px; 97 | } 98 | input[type="checkbox"], 99 | input[type="radio"] { 100 | border: 1px solid #ccc; 101 | } 102 | .form-horizontal .control-group > label { 103 | float: none; 104 | width: auto; 105 | padding-top: 0; 106 | text-align: left; 107 | } 108 | .form-horizontal .controls { 109 | margin-left: 0; 110 | } 111 | .form-horizontal .control-list { 112 | padding-top: 0; 113 | } 114 | .form-horizontal .form-actions { 115 | padding-right: 10px; 116 | padding-left: 10px; 117 | } 118 | .modal { 119 | position: absolute; 120 | top: 10px; 121 | right: 10px; 122 | left: 10px; 123 | width: auto; 124 | margin: 0; 125 | } 126 | .modal.fade.in { 127 | top: auto; 128 | } 129 | .modal-header .close { 130 | padding: 10px; 131 | margin: -10px; 132 | } 133 | .carousel-caption { 134 | position: static; 135 | } 136 | } 137 | 138 | @media (max-width: 767px) { 139 | body { 140 | padding-right: 20px; 141 | padding-left: 20px; 142 | } 143 | .navbar-fixed-top, 144 | .navbar-fixed-bottom { 145 | margin-right: -20px; 146 | margin-left: -20px; 147 | } 148 | .container-fluid { 149 | padding: 0; 150 | } 151 | .dl-horizontal dt { 152 | float: none; 153 | width: auto; 154 | clear: none; 155 | text-align: left; 156 | } 157 | .dl-horizontal dd { 158 | margin-left: 0; 159 | } 160 | .container { 161 | width: auto; 162 | } 163 | .row-fluid { 164 | width: 100%; 165 | } 166 | .row, 167 | .thumbnails { 168 | margin-left: 0; 169 | } 170 | [class*="span"], 171 | .row-fluid [class*="span"] { 172 | display: block; 173 | float: none; 174 | width: auto; 175 | margin-left: 0; 176 | } 177 | .input-large, 178 | .input-xlarge, 179 | .input-xxlarge, 180 | input[class*="span"], 181 | select[class*="span"], 182 | textarea[class*="span"], 183 | .uneditable-input { 184 | display: block; 185 | width: 100%; 186 | min-height: 28px; 187 | -webkit-box-sizing: border-box; 188 | -moz-box-sizing: border-box; 189 | -ms-box-sizing: border-box; 190 | box-sizing: border-box; 191 | } 192 | .input-prepend input, 193 | .input-append input, 194 | .input-prepend input[class*="span"], 195 | .input-append input[class*="span"] { 196 | display: inline-block; 197 | width: auto; 198 | } 199 | } 200 | 201 | @media (min-width: 768px) and (max-width: 979px) { 202 | .row { 203 | margin-left: -20px; 204 | *zoom: 1; 205 | } 206 | .row:before, 207 | .row:after { 208 | display: table; 209 | content: ""; 210 | } 211 | .row:after { 212 | clear: both; 213 | } 214 | [class*="span"] { 215 | float: left; 216 | margin-left: 20px; 217 | } 218 | .container, 219 | .navbar-fixed-top .container, 220 | .navbar-fixed-bottom .container { 221 | width: 724px; 222 | } 223 | .span12 { 224 | width: 724px; 225 | } 226 | .span11 { 227 | width: 662px; 228 | } 229 | .span10 { 230 | width: 600px; 231 | } 232 | .span9 { 233 | width: 538px; 234 | } 235 | .span8 { 236 | width: 476px; 237 | } 238 | .span7 { 239 | width: 414px; 240 | } 241 | .span6 { 242 | width: 352px; 243 | } 244 | .span5 { 245 | width: 290px; 246 | } 247 | .span4 { 248 | width: 228px; 249 | } 250 | .span3 { 251 | width: 166px; 252 | } 253 | .span2 { 254 | width: 104px; 255 | } 256 | .span1 { 257 | width: 42px; 258 | } 259 | .offset12 { 260 | margin-left: 764px; 261 | } 262 | .offset11 { 263 | margin-left: 702px; 264 | } 265 | .offset10 { 266 | margin-left: 640px; 267 | } 268 | .offset9 { 269 | margin-left: 578px; 270 | } 271 | .offset8 { 272 | margin-left: 516px; 273 | } 274 | .offset7 { 275 | margin-left: 454px; 276 | } 277 | .offset6 { 278 | margin-left: 392px; 279 | } 280 | .offset5 { 281 | margin-left: 330px; 282 | } 283 | .offset4 { 284 | margin-left: 268px; 285 | } 286 | .offset3 { 287 | margin-left: 206px; 288 | } 289 | .offset2 { 290 | margin-left: 144px; 291 | } 292 | .offset1 { 293 | margin-left: 82px; 294 | } 295 | .row-fluid { 296 | width: 100%; 297 | *zoom: 1; 298 | } 299 | .row-fluid:before, 300 | .row-fluid:after { 301 | display: table; 302 | content: ""; 303 | } 304 | .row-fluid:after { 305 | clear: both; 306 | } 307 | .row-fluid [class*="span"] { 308 | display: block; 309 | float: left; 310 | width: 100%; 311 | min-height: 28px; 312 | margin-left: 2.762430939%; 313 | *margin-left: 2.709239449638298%; 314 | -webkit-box-sizing: border-box; 315 | -moz-box-sizing: border-box; 316 | -ms-box-sizing: border-box; 317 | box-sizing: border-box; 318 | } 319 | .row-fluid [class*="span"]:first-child { 320 | margin-left: 0; 321 | } 322 | .row-fluid .span12 { 323 | width: 99.999999993%; 324 | *width: 99.9468085036383%; 325 | } 326 | .row-fluid .span11 { 327 | width: 91.436464082%; 328 | *width: 91.38327259263829%; 329 | } 330 | .row-fluid .span10 { 331 | width: 82.87292817100001%; 332 | *width: 82.8197366816383%; 333 | } 334 | .row-fluid .span9 { 335 | width: 74.30939226%; 336 | *width: 74.25620077063829%; 337 | } 338 | .row-fluid .span8 { 339 | width: 65.74585634900001%; 340 | *width: 65.6926648596383%; 341 | } 342 | .row-fluid .span7 { 343 | width: 57.182320438000005%; 344 | *width: 57.129128948638304%; 345 | } 346 | .row-fluid .span6 { 347 | width: 48.618784527%; 348 | *width: 48.5655930376383%; 349 | } 350 | .row-fluid .span5 { 351 | width: 40.055248616%; 352 | *width: 40.0020571266383%; 353 | } 354 | .row-fluid .span4 { 355 | width: 31.491712705%; 356 | *width: 31.4385212156383%; 357 | } 358 | .row-fluid .span3 { 359 | width: 22.928176794%; 360 | *width: 22.874985304638297%; 361 | } 362 | .row-fluid .span2 { 363 | width: 14.364640883%; 364 | *width: 14.311449393638298%; 365 | } 366 | .row-fluid .span1 { 367 | width: 5.801104972%; 368 | *width: 5.747913482638298%; 369 | } 370 | input, 371 | textarea, 372 | .uneditable-input { 373 | margin-left: 0; 374 | } 375 | input.span12, 376 | textarea.span12, 377 | .uneditable-input.span12 { 378 | width: 714px; 379 | } 380 | input.span11, 381 | textarea.span11, 382 | .uneditable-input.span11 { 383 | width: 652px; 384 | } 385 | input.span10, 386 | textarea.span10, 387 | .uneditable-input.span10 { 388 | width: 590px; 389 | } 390 | input.span9, 391 | textarea.span9, 392 | .uneditable-input.span9 { 393 | width: 528px; 394 | } 395 | input.span8, 396 | textarea.span8, 397 | .uneditable-input.span8 { 398 | width: 466px; 399 | } 400 | input.span7, 401 | textarea.span7, 402 | .uneditable-input.span7 { 403 | width: 404px; 404 | } 405 | input.span6, 406 | textarea.span6, 407 | .uneditable-input.span6 { 408 | width: 342px; 409 | } 410 | input.span5, 411 | textarea.span5, 412 | .uneditable-input.span5 { 413 | width: 280px; 414 | } 415 | input.span4, 416 | textarea.span4, 417 | .uneditable-input.span4 { 418 | width: 218px; 419 | } 420 | input.span3, 421 | textarea.span3, 422 | .uneditable-input.span3 { 423 | width: 156px; 424 | } 425 | input.span2, 426 | textarea.span2, 427 | .uneditable-input.span2 { 428 | width: 94px; 429 | } 430 | input.span1, 431 | textarea.span1, 432 | .uneditable-input.span1 { 433 | width: 32px; 434 | } 435 | } 436 | 437 | @media (min-width: 1200px) { 438 | .row { 439 | margin-left: -30px; 440 | *zoom: 1; 441 | } 442 | .row:before, 443 | .row:after { 444 | display: table; 445 | content: ""; 446 | } 447 | .row:after { 448 | clear: both; 449 | } 450 | [class*="span"] { 451 | float: left; 452 | margin-left: 30px; 453 | } 454 | .container, 455 | .navbar-fixed-top .container, 456 | .navbar-fixed-bottom .container { 457 | width: 1170px; 458 | } 459 | .span12 { 460 | width: 1170px; 461 | } 462 | .span11 { 463 | width: 1070px; 464 | } 465 | .span10 { 466 | width: 970px; 467 | } 468 | .span9 { 469 | width: 870px; 470 | } 471 | .span8 { 472 | width: 770px; 473 | } 474 | .span7 { 475 | width: 670px; 476 | } 477 | .span6 { 478 | width: 570px; 479 | } 480 | .span5 { 481 | width: 470px; 482 | } 483 | .span4 { 484 | width: 370px; 485 | } 486 | .span3 { 487 | width: 270px; 488 | } 489 | .span2 { 490 | width: 170px; 491 | } 492 | .span1 { 493 | width: 70px; 494 | } 495 | .offset12 { 496 | margin-left: 1230px; 497 | } 498 | .offset11 { 499 | margin-left: 1130px; 500 | } 501 | .offset10 { 502 | margin-left: 1030px; 503 | } 504 | .offset9 { 505 | margin-left: 930px; 506 | } 507 | .offset8 { 508 | margin-left: 830px; 509 | } 510 | .offset7 { 511 | margin-left: 730px; 512 | } 513 | .offset6 { 514 | margin-left: 630px; 515 | } 516 | .offset5 { 517 | margin-left: 530px; 518 | } 519 | .offset4 { 520 | margin-left: 430px; 521 | } 522 | .offset3 { 523 | margin-left: 330px; 524 | } 525 | .offset2 { 526 | margin-left: 230px; 527 | } 528 | .offset1 { 529 | margin-left: 130px; 530 | } 531 | .row-fluid { 532 | width: 100%; 533 | *zoom: 1; 534 | } 535 | .row-fluid:before, 536 | .row-fluid:after { 537 | display: table; 538 | content: ""; 539 | } 540 | .row-fluid:after { 541 | clear: both; 542 | } 543 | .row-fluid [class*="span"] { 544 | display: block; 545 | float: left; 546 | width: 100%; 547 | min-height: 28px; 548 | margin-left: 2.564102564%; 549 | *margin-left: 2.510911074638298%; 550 | -webkit-box-sizing: border-box; 551 | -moz-box-sizing: border-box; 552 | -ms-box-sizing: border-box; 553 | box-sizing: border-box; 554 | } 555 | .row-fluid [class*="span"]:first-child { 556 | margin-left: 0; 557 | } 558 | .row-fluid .span12 { 559 | width: 100%; 560 | *width: 99.94680851063829%; 561 | } 562 | .row-fluid .span11 { 563 | width: 91.45299145300001%; 564 | *width: 91.3997999636383%; 565 | } 566 | .row-fluid .span10 { 567 | width: 82.905982906%; 568 | *width: 82.8527914166383%; 569 | } 570 | .row-fluid .span9 { 571 | width: 74.358974359%; 572 | *width: 74.30578286963829%; 573 | } 574 | .row-fluid .span8 { 575 | width: 65.81196581200001%; 576 | *width: 65.7587743226383%; 577 | } 578 | .row-fluid .span7 { 579 | width: 57.264957265%; 580 | *width: 57.2117657756383%; 581 | } 582 | .row-fluid .span6 { 583 | width: 48.717948718%; 584 | *width: 48.6647572286383%; 585 | } 586 | .row-fluid .span5 { 587 | width: 40.170940171000005%; 588 | *width: 40.117748681638304%; 589 | } 590 | .row-fluid .span4 { 591 | width: 31.623931624%; 592 | *width: 31.5707401346383%; 593 | } 594 | .row-fluid .span3 { 595 | width: 23.076923077%; 596 | *width: 23.0237315876383%; 597 | } 598 | .row-fluid .span2 { 599 | width: 14.529914530000001%; 600 | *width: 14.4767230406383%; 601 | } 602 | .row-fluid .span1 { 603 | width: 5.982905983%; 604 | *width: 5.929714493638298%; 605 | } 606 | input, 607 | textarea, 608 | .uneditable-input { 609 | margin-left: 0; 610 | } 611 | input.span12, 612 | textarea.span12, 613 | .uneditable-input.span12 { 614 | width: 1160px; 615 | } 616 | input.span11, 617 | textarea.span11, 618 | .uneditable-input.span11 { 619 | width: 1060px; 620 | } 621 | input.span10, 622 | textarea.span10, 623 | .uneditable-input.span10 { 624 | width: 960px; 625 | } 626 | input.span9, 627 | textarea.span9, 628 | .uneditable-input.span9 { 629 | width: 860px; 630 | } 631 | input.span8, 632 | textarea.span8, 633 | .uneditable-input.span8 { 634 | width: 760px; 635 | } 636 | input.span7, 637 | textarea.span7, 638 | .uneditable-input.span7 { 639 | width: 660px; 640 | } 641 | input.span6, 642 | textarea.span6, 643 | .uneditable-input.span6 { 644 | width: 560px; 645 | } 646 | input.span5, 647 | textarea.span5, 648 | .uneditable-input.span5 { 649 | width: 460px; 650 | } 651 | input.span4, 652 | textarea.span4, 653 | .uneditable-input.span4 { 654 | width: 360px; 655 | } 656 | input.span3, 657 | textarea.span3, 658 | .uneditable-input.span3 { 659 | width: 260px; 660 | } 661 | input.span2, 662 | textarea.span2, 663 | .uneditable-input.span2 { 664 | width: 160px; 665 | } 666 | input.span1, 667 | textarea.span1, 668 | .uneditable-input.span1 { 669 | width: 60px; 670 | } 671 | .thumbnails { 672 | margin-left: -30px; 673 | } 674 | .thumbnails > li { 675 | margin-left: 30px; 676 | } 677 | .row-fluid .thumbnails { 678 | margin-left: 0; 679 | } 680 | } 681 | 682 | @media (max-width: 979px) { 683 | body { 684 | padding-top: 0; 685 | } 686 | .navbar-fixed-top { 687 | position: static; 688 | margin-bottom: 18px; 689 | } 690 | .navbar-fixed-top .navbar-inner { 691 | padding: 5px; 692 | } 693 | .navbar .container { 694 | width: auto; 695 | padding: 0; 696 | } 697 | .navbar .brand { 698 | padding-right: 10px; 699 | padding-left: 10px; 700 | margin: 0 0 0 -5px; 701 | } 702 | .nav-collapse { 703 | clear: both; 704 | } 705 | .nav-collapse .nav { 706 | float: none; 707 | margin: 0 0 9px; 708 | } 709 | .nav-collapse .nav > li { 710 | float: none; 711 | } 712 | .nav-collapse .nav > li > a { 713 | margin-bottom: 2px; 714 | } 715 | .nav-collapse .nav > .divider-vertical { 716 | display: none; 717 | } 718 | .nav-collapse .nav .nav-header { 719 | color: #999999; 720 | text-shadow: none; 721 | } 722 | .nav-collapse .nav > li > a, 723 | .nav-collapse .dropdown-menu a { 724 | padding: 6px 15px; 725 | font-weight: bold; 726 | color: #999999; 727 | -webkit-border-radius: 3px; 728 | -moz-border-radius: 3px; 729 | border-radius: 3px; 730 | } 731 | .nav-collapse .btn { 732 | padding: 4px 10px 4px; 733 | font-weight: normal; 734 | -webkit-border-radius: 4px; 735 | -moz-border-radius: 4px; 736 | border-radius: 4px; 737 | } 738 | .nav-collapse .dropdown-menu li + li a { 739 | margin-bottom: 2px; 740 | } 741 | .nav-collapse .nav > li > a:hover, 742 | .nav-collapse .dropdown-menu a:hover { 743 | background-color: #222222; 744 | } 745 | .nav-collapse.in .btn-group { 746 | padding: 0; 747 | margin-top: 5px; 748 | } 749 | .nav-collapse .dropdown-menu { 750 | position: static; 751 | top: auto; 752 | left: auto; 753 | display: block; 754 | float: none; 755 | max-width: none; 756 | padding: 0; 757 | margin: 0 15px; 758 | background-color: transparent; 759 | border: none; 760 | -webkit-border-radius: 0; 761 | -moz-border-radius: 0; 762 | border-radius: 0; 763 | -webkit-box-shadow: none; 764 | -moz-box-shadow: none; 765 | box-shadow: none; 766 | } 767 | .nav-collapse .dropdown-menu:before, 768 | .nav-collapse .dropdown-menu:after { 769 | display: none; 770 | } 771 | .nav-collapse .dropdown-menu .divider { 772 | display: none; 773 | } 774 | .nav-collapse .navbar-form, 775 | .nav-collapse .navbar-search { 776 | float: none; 777 | padding: 9px 15px; 778 | margin: 9px 0; 779 | border-top: 1px solid #222222; 780 | border-bottom: 1px solid #222222; 781 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 782 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 783 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 784 | } 785 | .navbar .nav-collapse .nav.pull-right { 786 | float: none; 787 | margin-left: 0; 788 | } 789 | .nav-collapse, 790 | .nav-collapse.collapse { 791 | height: 0; 792 | overflow: hidden; 793 | } 794 | .navbar .btn-navbar { 795 | display: block; 796 | } 797 | .navbar-static .navbar-inner { 798 | padding-right: 10px; 799 | padding-left: 10px; 800 | } 801 | } 802 | 803 | @media (min-width: 980px) { 804 | .nav-collapse.collapse { 805 | height: auto !important; 806 | overflow: visible !important; 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /module-code/public/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(a){a(function(){"use strict",a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",msTransition:"MSTransitionEnd",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=c,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(a){return a||(this.paused=!0),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j=a.Event("slide");this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c);e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():f.interval&&e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning)return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning)return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e,f,g;if(c.is(".disabled, :disabled"))return;return f=c.attr("data-target"),f||(f=c.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,"")),e=a(f),e.length||(e=c.parent()),g=e.hasClass("open"),d(),g||e.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('