├── DEVELOPMENT.md ├── README.md ├── app ├── Global.scala ├── controllers │ ├── Application.scala │ ├── Apps.scala │ ├── Auth.scala │ ├── CityAPI.scala │ ├── Clients.scala │ ├── OAuth2Controller.scala │ ├── SampleAPI.scala │ ├── Secured.scala │ └── SecuredOAuth.scala ├── models │ ├── Helpers.scala │ └── oauth2 │ │ ├── AccessToken.scala │ │ ├── AuthCode.scala │ │ ├── Client.scala │ │ ├── ClientGrantType.scala │ │ ├── GrantType.scala │ │ └── User.scala ├── oauth2 │ ├── Crypto.scala │ └── OAuthDataHandler.scala └── views │ ├── apis.scala.html │ ├── apps │ └── authorize.scala.html │ ├── clients │ ├── edit_client.scala.html │ ├── list.scala.html │ ├── new_client.scala.html │ └── show_client.scala.html │ ├── docs.scala.html │ ├── index.scala.html │ ├── login.scala.html │ └── main.scala.html ├── build.sbt ├── conf ├── application.conf ├── application.dev.conf ├── application.prod.conf ├── application.test.conf ├── evolutions │ └── default │ │ └── 1.sql ├── logger.xml └── routes ├── docs ├── fixtures.sql ├── oauth2-actors.dia ├── oauth2-actors.png ├── oauth2-actors.svg ├── oauth2-api-call.dia ├── oauth2-api-call.png ├── oauth2-api-call.svg ├── oauth2-client-registration.dia ├── oauth2-client-registration.png └── oauth2-client-registration.svg ├── project ├── build.properties └── plugins.sbt ├── public ├── css │ ├── custom.css │ └── main.css └── images │ └── favicon.png ├── sample-apps └── sample-app1 │ ├── .gitignore │ ├── README │ ├── app │ ├── Global.scala │ ├── controllers │ │ └── Application.scala │ └── views │ │ ├── index.scala.html │ │ └── main.scala.html │ ├── build.sbt │ ├── conf │ ├── application.conf │ └── routes │ ├── project │ ├── build.properties │ └── plugins.sbt │ ├── public │ ├── css │ │ ├── custom.css │ │ └── main.css │ └── js │ │ └── main.js │ └── test │ ├── ApplicationSpec.scala │ └── IntegrationSpec.scala └── test ├── ApplicationSpec.scala ├── IntegrationSpec.scala └── models ├── Fixtures.scala └── oauth2 └── UsersSpec.scala /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Usual OAuth2.0 Workflow: 2 | 3 | Usual OAuth2.0 Workflow steps: 4 | 5 | * 1: Let the user know what you're doing and request authorization 6 | * 2: Exchange authorization code for an access token 7 | * 3: Call the API 8 | * 4a: Refresh the access token 9 | * 4b: Obtaining a new access token 10 | 11 | 12 | # First time setup 13 | 14 | Create the database: 15 | 16 | $ mkdir db 17 | $ cd db 18 | $ sqlite3 oauth2server_dev_db.sqlite 19 | 20 | 21 | 22 | Start the server: 23 | 24 | $ sbt "run 9002" 25 | 26 | This will start the server at port 9002. Now goto http://localhost:9002/ 27 | 28 | For the first time, you would see a message: `Database 'default' needs evolution!`. Now click "Apply this script now!" 29 | 30 | 31 | # Use fixtures.sql to fill up some sample data. 32 | 33 | In a separate terminal, go to Sqlite3 console and run queries from `fixtures.sql`. 34 | 35 | $ sqlite3 db/oauth2server_dev_db.sqlite 36 | sqlite> .read docs/fixtures.sql 37 | 38 | Now you should be able to access the server using user `user1` and password `password`. 39 | 40 | # Ensure that the server is running on port 9002 41 | 42 | $ play 43 | [play-oauth2-server] $ run 9002 44 | 45 | # OAuth2.0 Workflows using wget 46 | 47 | ## Generate an access token using password grant_type 48 | 49 | $ wget -q -O - --post-data "grant_type=password&client_id=client1&client_secret=secret1&username=user1&password=password" http://localhost:9002/oauth2/access_token | python -mjson.tool 50 | { 51 | "access_token": "MzE3YWI5MTUtZWEwNy00OTU1LTgyMTQtZmE2ZjBlMzQwYzYx", 52 | "expires_in": 3600, 53 | "refresh_token": "NmRmYjg1NzItMzc0YS00YTgzLTk0OWItMmFjNjQxM2U1NjFk", 54 | "scope": "", 55 | "token_type": "Bearer" 56 | } 57 | 58 | 59 | 60 | ## Use protected resource using the token: 61 | 62 | 63 | $ wget -q -d --header="Authorization: Bearer MzE3YWI5MTUtZWEwNy00OTU1LTgyMTQtZmE2ZjBlMzQwYzYx" "http://localhost:9002/sampleapi/status/123" -O - 64 | 65 | ---request begin--- 66 | GET /sampleapi/status/123 HTTP/1.1 67 | User-Agent: Wget/1.15 (linux-gnu) 68 | Accept: */* 69 | Host: localhost:9002 70 | Connection: Keep-Alive 71 | Authorization: Bearer MzE3YWI5MTUtZWEwNy00OTU1LTgyMTQtZmE2ZjBlMzQwYzYx 72 | 73 | ---request end--- 74 | 75 | ---response begin--- 76 | HTTP/1.1 200 OK 77 | Content-Type: application/json; charset=utf-8 78 | Content-Length: 43 79 | 80 | ---response end--- 81 | 82 | {"id":"123","total":"100","completed":"30"} 83 | 84 | 85 | 86 | 87 | ## Use a Refresh Token to get a new token 88 | 89 | $ wget -d -q -O - --post-data \ 90 | "grant_type=refresh_token&client_id=client1&client_secret=secret1&refresh_token=NmRmYjg1NzItMzc0YS00YTgzLTk0OWItMmFjNjQxM2U1NjFk" http://localhost:9002/oauth2/access_token 91 | 92 | ---request begin--- 93 | POST /oauth2/access_token HTTP/1.1 94 | User-Agent: Wget/1.15 (linux-gnu) 95 | Accept: */* 96 | Host: localhost:9002 97 | Connection: Keep-Alive 98 | Content-Type: application/x-www-form-urlencoded 99 | Content-Length: 127 100 | 101 | ---request end--- 102 | [BODY data: grant_type=refresh_token&client_id=client1&client_secret=secret1&refresh_token=NmRmYjg1NzItMzc0YS00YTgzLTk0OWItMmFjNjQxM2U1NjFk] 103 | 104 | ---response begin--- 105 | HTTP/1.1 200 OK 106 | Content-Type: application/json; charset=utf-8 107 | Content-Length: 185 108 | 109 | ---response end--- 110 | 111 | { 112 | "access_token":"MTkyNGY2MzYtMWM3OS00YjhkLTg2OTktMDQzOGQyMjU2NmM5", 113 | "expires_in":3600, 114 | "scope":"", 115 | "refresh_token":"YjQ1NTZmOWUtYmYwNS00ZmNkLWIwN2MtMTEwMjcwNTdlYjgw", 116 | "token_type":"Bearer" 117 | } 118 | 119 | 120 | ## When the access token expires we get an error in the header. 121 | 122 | $ wget -q -d --header="Authorization: Bearer MTkyNGY2MzYtMWM3OS00YjhkLTg2OTktMDQzOGQyMjU2NmM5" "http://localhost:9002/sampleapi/status/123" -O - 123 | 124 | ---request begin--- 125 | GET /sampleapi/status/123 HTTP/1.1 126 | User-Agent: Wget/1.15 (linux-gnu) 127 | Accept: */* 128 | Host: localhost:9002 129 | Connection: Keep-Alive 130 | Authorization: Bearer MTkyNGY2MzYtMWM3OS00YjhkLTg2OTktMDQzOGQyMjU2NmM5 131 | 132 | ---request end--- 133 | 134 | ---response begin--- 135 | HTTP/1.1 401 Unauthorized 136 | WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired" 137 | Content-Length: 0 138 | 139 | ---response end--- 140 | 141 | 142 | ## Generate a token via authorization code workflow 143 | 144 | 145 | $ wget -d -q -O - --post-data "grant_type=authorization_code&client_id=client1&client_secret=secret1&code=authcode1&redirect_uri=http://localhost:9001/" http://localhost:9002/oauth2/access_token | python -mjson.tool 146 | 147 | ---request begin--- 148 | POST /oauth2/access_token HTTP/1.1 149 | User-Agent: Wget/1.15 (linux-gnu) 150 | Accept: */* 151 | Host: localhost:9002 152 | Connection: Keep-Alive 153 | Content-Type: application/x-www-form-urlencoded 154 | Content-Length: 120 155 | 156 | ---request end--- 157 | 158 | ---response begin--- 159 | HTTP/1.1 200 OK 160 | Content-Type: application/json; charset=utf-8 161 | Content-Length: 185 162 | 163 | ---response end--- 164 | 165 | { 166 | "access_token": "ZjlmZTE5OGEtNDM5Yi00ODczLWIxYzEtOTk5M2RhNmU5MTIy", 167 | "expires_in": 3600, 168 | "refresh_token": "OTY0MzU5NmMtNTlkOC00ZmVhLTg4OTctZjYyYzk0MDU2ZGMz", 169 | "scope": "", 170 | "token_type": "Bearer" 171 | } 172 | 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oauth2 Server using Play! 2 | 3 | Also have a look at OAuth2 client apps in sample-apps/ folder. 4 | 5 | ## Requirements 6 | 7 | This project requires following tools 8 | 9 | * JDK7+ 10 | * SBT 0.13+ 11 | 12 | ## Build 13 | 14 | Run tests 15 | 16 | $ sbt test 17 | 18 | 19 | Create a distribution 20 | 21 | $ sbt dist 22 | 23 | ## First time Server and Database Setup 24 | 25 | Please read `DEVELOPMENT.md` for instructions [DEVELOPMENT.md](DEVELOPMENT.md). 26 | 27 | 28 | ## Test Coverage 29 | 30 | SBT Scoverage plugin generates the test coverage: 31 | 32 | $ sbt scoverage:test 33 | 34 | 35 | The report is generated in XML and HTML format at `target/scala-2.10/scoverage-report/` 36 | 37 | $ cd target/scala-2.10/scoverage-report/ 38 | $ firefox /index.html 39 | 40 | ## Deployment 41 | 42 | [Read here](http://www.playframework.com/documentation/2.0/Production) 43 | 44 | -------------------------------------------------------------------------------- /app/Global.scala: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import play.api._ 3 | import com.typesafe.config.ConfigFactory 4 | import java.util.Date 5 | import java.sql.Timestamp 6 | 7 | object Global extends GlobalSettings { 8 | 9 | override def onStart(app: Application) { 10 | super.onStart(app) 11 | val isLoadFixtures = app.configuration.getBoolean("fixtures.populateOnStartUp").getOrElse(false) 12 | println("fixtures enabled: " + isLoadFixtures) 13 | if (isLoadFixtures) { 14 | /// load sample data here 15 | } 16 | } 17 | 18 | override def onLoadConfig(config: Configuration, 19 | path: File, classloader: ClassLoader, 20 | mode: Mode.Mode): Configuration = { 21 | 22 | val configfile = s"application.${mode.toString.toLowerCase}.conf" 23 | println(s"Mode is ${mode.toString}, Loading: ${configfile}") 24 | val modeSpecificConfig = config ++ Configuration( 25 | ConfigFactory.load(configfile)) 26 | super.onLoadConfig(modeSpecificConfig, path, classloader, mode) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | 6 | object Application extends Controller with Secured { 7 | 8 | def index = withUser { user => 9 | implicit request => 10 | Ok(views.html.index("Welcome to OAuth2 Server running on Play! 2.0 Framework.", user)) 11 | } 12 | 13 | def apis = withUser { user => 14 | implicit request => 15 | Ok(views.html.apis("API Listing", user)) 16 | } 17 | 18 | def docs = withUser { user => 19 | implicit request => 20 | Ok(views.html.docs("API documentation", user)) 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /app/controllers/Apps.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc.{ Action, Controller } 4 | import models.oauth2.Client 5 | import play.api.db.slick.DBAction 6 | import play.api.data._ 7 | import play.api.data.Forms._ 8 | import play.api.Play.current 9 | import play.api.db.slick.DB 10 | import oauth2.OAuthDataHandler 11 | 12 | case class AppAuthInfo(clientId: String, redirectUri: String, 13 | scope: String, state: String, accepted: String) 14 | 15 | object Apps extends Controller with Secured { 16 | val log = play.Logger.of("application") 17 | 18 | val errorCodes = Map( 19 | "access_denied" -> "Access was denied", 20 | "invalid_request" -> "Request made was not valid", 21 | "unauthorized_client" -> "Client is not authorized to perform this action", 22 | "unsupported_response_type" -> "Response type requested is not allowed", 23 | "invalid_scope" -> "Requested scope is not allowed", 24 | "server_error" -> "Server encountered an error", 25 | "temporarily_unavailable" -> "Service is temporary unavailable") 26 | 27 | val AppAuthInfoForm = Form(mapping( 28 | "client_id" -> nonEmptyText, 29 | "redirect_uri" -> nonEmptyText, 30 | "scope" -> text, 31 | "state" -> text, 32 | "accepted" -> text)(AppAuthInfo.apply)(AppAuthInfo.unapply)) 33 | 34 | def authorize = withUser { user => 35 | implicit request => 36 | // read URL parameters 37 | val params = List("client_id", "redirect_uri", "state", "scope") 38 | val data = params.map(k => 39 | (k -> request.queryString.get(k).getOrElse(Seq("")).head)).toMap 40 | 41 | // check if such a client exists 42 | DB.withSession { implicit session => 43 | val clientId = data("client_id") 44 | models.oauth2.Clients.findByClientId(clientId) match { 45 | case None => // doesn't exist 46 | BadRequest("No such client exists.") 47 | case Some(client) => 48 | val aaInfoForm = AppAuthInfoForm.bind(data) 49 | 50 | log.debug(aaInfoForm.data.toString) 51 | data.keys.foreach { k => 52 | log.debug(k) 53 | log.debug(aaInfoForm(k).value.toString) 54 | } 55 | Ok(views.html.apps.authorize(user, aaInfoForm)) 56 | } 57 | } 58 | } 59 | 60 | def send_auth = withUser { user => 61 | implicit request => 62 | val boundForm = AppAuthInfoForm.bindFromRequest 63 | boundForm.fold( 64 | formWithErrors => { 65 | log.debug(formWithErrors.toString) 66 | Ok(views.html.apps.authorize(user, formWithErrors)) 67 | }, 68 | aaInfo => { 69 | aaInfo.accepted match { 70 | case "Y" => 71 | val expiresIn = Int.MaxValue 72 | val acOpt = 73 | DB.withSession { implicit session => 74 | models.oauth2.AuthCodes.generateAuthCodeForClient( 75 | aaInfo.clientId, aaInfo.redirectUri, aaInfo.scope, 76 | user.id.get, expiresIn) 77 | } 78 | acOpt match { 79 | case Some(ac) => 80 | val authCode = ac.authorizationCode 81 | val state = aaInfo.state 82 | Redirect(s"${aaInfo.redirectUri}?code=${authCode}&state=${state}") 83 | case None => 84 | val errorCode = "server_error" 85 | Redirect(s"${aaInfo.redirectUri}?error=${errorCode}") 86 | } 87 | 88 | case "N" => 89 | val errorCode = "access_denied" 90 | Redirect(s"${aaInfo.redirectUri}?error=${errorCode}") 91 | case _ => 92 | val errorCode = "invalid_request" 93 | Redirect(s"${aaInfo.redirectUri}?error=${errorCode}") 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/controllers/Auth.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import play.api.data._ 6 | import play.api.data.Forms._ 7 | import models.Helpers 8 | 9 | object Auth extends Controller { 10 | val log = play.Logger.of("application") 11 | 12 | val loginForm = Form( 13 | tuple( 14 | "username" -> text, 15 | "password" -> text, 16 | "redirect_url" -> text) verifying ("Invalid email or password", result => result match { 17 | case (username, password, _) => check(username, password) 18 | })) 19 | 20 | def check(username: String, password: String) = { 21 | import play.api.Play.current 22 | import play.api.db.slick.DB 23 | 24 | DB.withSession { implicit session => 25 | val encodedPass = Helpers.encodePassword(password) 26 | val u = models.oauth2.Users.findByUsernameAndPassword(username, encodedPass) 27 | u match { 28 | case None => false 29 | case Some(user) => true 30 | } 31 | } 32 | } 33 | 34 | def login = Action { implicit request => 35 | val boundForm = loginForm.bind(Map("redirect_url" -> request.flash.get("redirect_url").getOrElse("/"))) 36 | Ok(views.html.login(boundForm)) 37 | } 38 | 39 | def authenticate = Action { implicit request => 40 | val boundForm = loginForm.bindFromRequest 41 | 42 | boundForm.fold( 43 | formWithErrors => { 44 | log.debug("Form has errors: Invalid username or password") 45 | BadRequest(views.html.login(formWithErrors)) 46 | }, 47 | 48 | user => { 49 | val (username, password, redirectUrl) = user 50 | log.debug("Logged in !") 51 | 52 | (redirectUrl match { 53 | case "/" => 54 | Redirect(routes.Application.index) 55 | case _ => 56 | Redirect(redirectUrl) 57 | }).withSession(Security.username -> username) 58 | 59 | }) 60 | } 61 | 62 | def logout = Action { 63 | Redirect(routes.Auth.login).withNewSession.flashing( 64 | "success" -> "You are now logged out.") 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /app/controllers/CityAPI.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc.{ Action, Controller } 4 | import play.api.libs.json.Json 5 | import scalaoauth2.provider.OAuth2Provider 6 | import oauth2.OAuthDataHandler 7 | import play.api.db.slick.DBAction 8 | import play.api.mvc.Result 9 | import play.api.mvc.SimpleResult 10 | import play.api.db.slick.DB 11 | import play.api.Play.current 12 | 13 | object CityAPI extends Controller with SecuredOAuth { 14 | 15 | def findById(id: Long) = DBAction { implicit rs => 16 | val result: Result = authorize(new OAuthDataHandler()) { authInfo => 17 | Ok 18 | } 19 | // TODO: This is a dirty way. Fix the DBAction result. 20 | result.asInstanceOf[SimpleResult] 21 | } 22 | 23 | // Another alternative is to directly use slick DB.withSession 24 | def findById1(id: Long) = authorizedReadAction { implicit request => 25 | authInfo => 26 | DB.withSession { implicit session => 27 | // perform some read from database 28 | Ok 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/controllers/Clients.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc.{ Action, Controller } 4 | import models.oauth2.Client 5 | import play.api.db.slick.DBAction 6 | import play.api.data._ 7 | import play.api.data.Forms._ 8 | import play.api.Play.current 9 | import play.api.db.slick.DB 10 | import java.util.UUID 11 | import oauth2.Crypto 12 | 13 | case class ClientDetails(id: String, secret: String, description: String, redirectUri: String, scope: String) 14 | 15 | object Clients extends Controller with Secured { 16 | val log = play.Logger.of("application") 17 | 18 | val clientForm = Form(mapping( 19 | "id" -> nonEmptyText, 20 | "secret" -> nonEmptyText, 21 | "description" -> nonEmptyText, 22 | "redirectUri" -> nonEmptyText, 23 | "scope" -> text)(ClientDetails.apply)(ClientDetails.unapply)) 24 | 25 | def list = withUser { user => 26 | implicit request => 27 | log.debug("Clients.list " + request.method) 28 | 29 | val allClients = DB.withSession { implicit session => 30 | val clientsFromDb = models.oauth2.Clients.findByUser(user.username) 31 | println(clientsFromDb) 32 | clientsFromDb 33 | } 34 | 35 | Ok(views.html.clients.list(allClients, user)) 36 | } 37 | 38 | def create() = withUser { user => 39 | implicit request => 40 | log.debug("Clients.newClient " + request.method) 41 | 42 | // generate unique and random values for id and secret 43 | val clientId = Crypto.generateUUID 44 | val clientSecret = Crypto.generateUUID 45 | val boundForm = clientForm.bind(Map("id" -> clientId, "secret" -> clientSecret)) 46 | 47 | Ok(views.html.clients.new_client(boundForm, user)) 48 | } 49 | 50 | def edit(id: String) = withUser { user => 51 | implicit request => 52 | log.debug("Clients.newClient " + request.method) 53 | 54 | DB.withSession { implicit session => 55 | val clientOpt = models.oauth2.Clients.get(id) 56 | clientOpt match { 57 | case None => NotFound 58 | case Some(client) => 59 | val boundForm = clientForm.bind(Map("id" -> client.id, "secret" -> client.secret, 60 | "description" -> client.description, "redirectUri" -> client.redirectUri, 61 | "scope" -> client.scope)) 62 | 63 | Ok(views.html.clients.edit_client(boundForm, user)) 64 | } 65 | } 66 | } 67 | 68 | def get(id: String) = withUser { user => 69 | implicit request => 70 | log.debug("Clients.get " + request.method) 71 | DB.withSession { implicit session => 72 | val clientOpt = models.oauth2.Clients.get(id) 73 | clientOpt match { 74 | case None => NotFound 75 | case Some(client) => 76 | Ok(views.html.clients.show_client(client, user)) 77 | } 78 | } 79 | } 80 | 81 | def delete(id: String) = withUser { user => 82 | implicit request => 83 | log.debug("Clients.delete " + request.method) 84 | NotImplemented 85 | } 86 | 87 | def add = withUser { user => 88 | implicit request => 89 | log.debug("Clients.add " + request.method) 90 | request.method match { 91 | case "POST" => 92 | log.debug(request.body.toString) 93 | 94 | val boundForm = clientForm.bindFromRequest 95 | boundForm.fold( 96 | 97 | // validate rules 98 | // form has errors 99 | formWithErrors => { 100 | log.debug("Form has errors") 101 | log.debug(formWithErrors.errors.toString) 102 | Ok(views.html.clients.new_client(formWithErrors, user)).flashing( 103 | "error" -> "Form has errors. Please enter correct values.") 104 | }, 105 | 106 | clientDetails => { 107 | // check for duplicate 108 | log.debug(clientDetails.toString) 109 | DB.withSession { implicit session => 110 | models.oauth2.Clients.get(clientDetails.id) match { 111 | case None => 112 | // save 113 | log.debug("Saving new client") 114 | val client = models.oauth2.Client(clientDetails.id, user.username, clientDetails.secret, 115 | clientDetails.description, clientDetails.redirectUri, clientDetails.scope) 116 | models.oauth2.Clients.insert(client) 117 | // redirect to list 118 | log.debug("redirecting to client list") 119 | Redirect(routes.Clients.list) 120 | case Some(c) => // duplicate 121 | log.debug("Duplicate client entry") 122 | val flash = play.api.mvc.Flash(Map("error" -> "Please select another id for this client")) 123 | Ok(views.html.clients.new_client(boundForm, user)(flash)) 124 | 125 | } 126 | } 127 | }) 128 | 129 | case "PUT" => 130 | NotImplemented 131 | } 132 | } 133 | 134 | def update = withUser { user => 135 | implicit request => 136 | 137 | log.debug("Clients.update()") 138 | val boundForm = clientForm.bindFromRequest 139 | boundForm.fold( 140 | 141 | formWithErrors => { // validate rules 142 | log.debug("Form has errors") 143 | log.debug(formWithErrors.errors.toString) 144 | Ok(views.html.clients.new_client(formWithErrors, user)).flashing( 145 | "error" -> "Form has errors. Please enter correct values.") 146 | }, 147 | 148 | clientDetails => { 149 | val cd = clientDetails 150 | log.debug("Client details") 151 | // log.debug((cd.id, cd.secret, cd.redirectUi, cd.scope, cd.description).toString) 152 | DB.withSession { implicit session => 153 | 154 | // log.debug("Searching for client: " + cd.id) 155 | models.oauth2.Clients.get(cd.id) match { 156 | case Some(c) => // existing client 157 | log.debug("Saving new client") 158 | 159 | val client = models.oauth2.Client( 160 | cd.id, user.username, cd.secret, 161 | cd.description, cd.redirectUri, cd.scope) 162 | 163 | models.oauth2.Clients.update(client) // save 164 | log.debug("redirecting to client list") 165 | Redirect(routes.Clients.list) // redirect to client listing 166 | case None => // does not exist 167 | log.debug("No such client") 168 | NotFound 169 | } 170 | } 171 | }) 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /app/controllers/OAuth2Controller.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | import scalaoauth2.provider._ 3 | import play.api.mvc.{ Action, Controller } 4 | import oauth2.OAuthDataHandler 5 | 6 | object OAuth2Controller extends Controller with OAuth2Provider { 7 | def accessToken = Action { implicit request => 8 | 9 | val log = play.Logger.of("application") 10 | 11 | log.debug("Invoked OAuth2Controller request") 12 | 13 | issueAccessToken(new OAuthDataHandler()) 14 | } 15 | } -------------------------------------------------------------------------------- /app/controllers/SampleAPI.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc.{ Action, Controller } 4 | import play.api.libs.json.Json 5 | import scalaoauth2.provider.OAuth2Provider 6 | import oauth2.OAuthDataHandler 7 | 8 | object SampleAPI extends Controller with OAuth2Provider { 9 | 10 | def status(id: String) = Action { implicit request => 11 | 12 | authorize(new OAuthDataHandler()) { authInfo => 13 | 14 | val user = authInfo.user 15 | 16 | val sampleData = Set("123", "345", "567", "789", "890") 17 | 18 | if (sampleData.contains(id)) { 19 | val responseData = Map("id" -> id, "total" -> "100", "completed" -> "30") 20 | Ok(Json.toJson(responseData)) 21 | } else { 22 | NotFound 23 | } 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/controllers/Secured.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc.Security 4 | import play.api.mvc.RequestHeader 5 | import play.api.mvc.Results 6 | import play.api.mvc.Result 7 | import play.api.mvc.Request 8 | import play.api._ 9 | import play.api.mvc._ 10 | import play.api.data._ 11 | import play.api.data.Forms._ 12 | 13 | /** 14 | * Provide security features 15 | */ 16 | trait Secured { 17 | 18 | /** 19 | * Retrieve the connected user. 20 | */ 21 | private def username(request: RequestHeader) = request.session.get("username") 22 | 23 | /** 24 | * Redirect to login if the user in not authorized. 25 | */ 26 | private def onUnauthorized(request: RequestHeader) = { 27 | 28 | // capture the original URL we want to be redirected to later on successful login 29 | val args: Seq[(String, String)] = if (request.method.equals("GET")) Seq("redirect_url" -> request.uri) else Seq() 30 | 31 | Results.Redirect(routes.Auth.login).flashing(args: _*) 32 | } 33 | 34 | // -- 35 | 36 | def withAuth(f: => String => Request[AnyContent] => Result) = { 37 | Security.Authenticated(username, onUnauthorized) { user => 38 | Action(request => f(user)(request)) 39 | } 40 | } 41 | 42 | /** 43 | * This method shows how you could wrap the withAuth method to also fetch your user 44 | * You will need to implement UserDAO.findOneByUsername 45 | */ 46 | def withUser(f: models.oauth2.User => Request[AnyContent] => Result) = withAuth { username => 47 | implicit request => 48 | import play.api.Play.current 49 | import play.api.db.slick.DB 50 | DB.withSession { implicit session => 51 | models.oauth2.Users.findByUsername(username).map { user => 52 | f(user)(request) 53 | }.getOrElse(onUnauthorized(request)) 54 | } 55 | } 56 | 57 | /** 58 | * Action for authenticated users. 59 | */ 60 | def IsAuthenticated(f: => String => Request[AnyContent] => Result) = Security.Authenticated(username, onUnauthorized) { user => 61 | Action(request => f(user)(request)) 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/controllers/SecuredOAuth.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import scalaoauth2.provider.OAuth2Provider 4 | import play.api.mvc.Request 5 | import play.api.mvc.AnyContent 6 | import play.api.mvc.RequestHeader 7 | import play.api.mvc.Results 8 | import play.api.mvc.Result 9 | import play.api.mvc.Action 10 | import play.api.mvc.Security 11 | import play.api.mvc.SimpleResult 12 | import scalaoauth2.provider.AuthInfo 13 | import oauth2.OAuthDataHandler 14 | import models.oauth2.User 15 | 16 | trait SecuredOAuth extends Secured with OAuth2Provider { 17 | 18 | /** 19 | * Create an action which only succeeds if authorized according to OAuth2.0 20 | * 21 | * @param f Function which will be called on this action. 22 | * @return Action 23 | */ 24 | def authorizedAction(f: (Request[AnyContent] => (AuthInfo[User] => Result))) = Action { 25 | implicit request => 26 | authorize(new OAuthDataHandler()) { authInfo => 27 | f(request)(authInfo) 28 | } 29 | } 30 | 31 | /** 32 | * Create an action that is only allowed for the scopes in allowedScopes set 33 | * 34 | * @param allowedScopes A Set of scope strings 35 | * @param f Function which will be called on this action. 36 | * @return Action 37 | */ 38 | def authorizedScopedAction(allowedScopes: Set[String])(f: (Request[AnyContent] => (AuthInfo[User] => Result))) = Action { 39 | implicit request => 40 | authorize(new OAuthDataHandler()) { authInfo => 41 | authInfo.scope.filter(x => allowedScopes.contains(x)).map { sc => 42 | f(request)(authInfo) 43 | }.getOrElse(Unauthorized) 44 | } 45 | } 46 | 47 | /** 48 | * Create an action that is only allowed for read scope 49 | * 50 | * @param f Function which will be called on this action. 51 | * @return Action 52 | */ 53 | def authorizedReadAction(f: (Request[AnyContent] => (AuthInfo[User] => Result))) = authorizedScopedAction(Set("read", "readwrite"))(f) 54 | 55 | /** 56 | * Create an action that is only allowed for write scope 57 | * 58 | * @param f Function which will be called on this action. 59 | * @return Action 60 | */ 61 | def authorizedWriteAction(f: (Request[AnyContent] => (AuthInfo[User] => Result))) = authorizedScopedAction(Set("readwrite"))(f) 62 | 63 | /** 64 | * Create an action that is only allowed for the given scope string 65 | * 66 | * @param f Function which will be called on this action. 67 | * @return Action 68 | */ 69 | def authorizedAction(scope: String)(f: (Request[AnyContent] => (AuthInfo[User] => Result))) = authorizedScopedAction(Set(scope))(f) 70 | 71 | } -------------------------------------------------------------------------------- /app/models/Helpers.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import play.api.libs.Codecs 4 | 5 | object Helpers { 6 | 7 | /** 8 | * @param password A string of password characters 9 | * @return SHA1 of @password represented as Hex characters 10 | */ 11 | def encodePassword(password: String) = { 12 | Codecs.sha1(password) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/models/oauth2/AccessToken.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | import scala.slick.lifted.Tag 5 | import java.util.Date 6 | import java.sql.Timestamp 7 | import oauth2.Crypto 8 | 9 | case class AccessToken(id: Option[Int], accessToken: String, 10 | refreshToken: String, clientId: String, userId: Int, scope: String, 11 | expiresIn: Long, createdAt: java.sql.Timestamp) 12 | 13 | class AccessTokens(tag: Tag) extends Table[AccessToken](tag, "access_tokens") { 14 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc, O.NotNull) 15 | def accessToken = column[String]("token", O.NotNull) 16 | def refreshToken = column[String]("refresh_token", O.NotNull) 17 | def clientId = column[String]("client_id", O.NotNull) 18 | def userId = column[Int]("user_id") 19 | def scope = column[String]("scope", O.NotNull) 20 | def expiresIn = column[Long]("expires_in", O.NotNull) 21 | def createdAt = column[java.sql.Timestamp]("created_at", O.NotNull) 22 | def * = (id.?, accessToken, refreshToken, 23 | clientId, userId, scope, expiresIn, createdAt) <> 24 | (AccessToken.tupled, AccessToken.unapply _) 25 | } 26 | 27 | object AccessTokens { 28 | val accessTokens = TableQuery[AccessTokens] 29 | 30 | /** 31 | * Fetch AccessToken by its ID. 32 | * @param id 33 | * @param session 34 | * @return 35 | */ 36 | def get(id: Int)(implicit session: Session): Option[AccessToken] = 37 | accessTokens.where(_.id === id).firstOption 38 | 39 | /** 40 | * Find AccessToken by token value 41 | * @param accessToken 42 | * @param session 43 | * @return 44 | */ 45 | def find(accessToken: String)(implicit session: Session): Option[AccessToken] = 46 | accessTokens.where(_.accessToken === accessToken).firstOption 47 | 48 | /** 49 | * Find AccessToken by User and Client 50 | * @param userId 51 | * @param clientId 52 | * @param session 53 | * @return 54 | */ 55 | def findByUserAndClient(userId: Int, clientId: String)(implicit session: Session): Option[AccessToken] = 56 | accessTokens.where(a => a.userId === userId && a.clientId === clientId).firstOption 57 | 58 | /** 59 | * Find Refresh Token by its value 60 | * @param refreshToken 61 | * @param session 62 | * @return 63 | */ 64 | def findByRefreshToken(refreshToken: String)(implicit session: Session): Option[AccessToken] = 65 | accessTokens.where(_.refreshToken === refreshToken).firstOption 66 | 67 | /** 68 | * Add a new AccessToken 69 | * @param token 70 | * @param session 71 | * @return 72 | */ 73 | def insert(token: AccessToken)(implicit session: Session) = { 74 | token.id match { 75 | case None => (accessTokens returning accessTokens.map(_.id)) += token 76 | case Some(x) => accessTokens += token 77 | } 78 | } 79 | 80 | /** 81 | * Update existing AccessToken associated with a user and a client. 82 | * @param accessToken 83 | * @param userId 84 | * @param clientId 85 | * @param session 86 | * @return 87 | */ 88 | def updateByUserAndClient(accessToken: AccessToken, userId: Int, clientId: String)(implicit session: Session) = { 89 | session.withTransaction { 90 | accessTokens.where(a => a.clientId === clientId && a.userId === userId).delete 91 | accessTokens.insert(accessToken) 92 | } 93 | } 94 | 95 | /** 96 | * Update AccessToken object based for the ID in accessToken object 97 | * @param accessToken 98 | * @param session 99 | * @return 100 | */ 101 | def update(accessToken: AccessToken)(implicit session: Session) = { 102 | accessTokens.where(_.id === accessToken.id).update(accessToken) 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /app/models/oauth2/AuthCode.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | import scala.slick.lifted.Tag 5 | import java.util.Date 6 | import java.sql.Timestamp 7 | import oauth2.Crypto 8 | 9 | case class AuthCode(authorizationCode: String, userId: Int, 10 | redirectUri: Option[String], createdAt: java.sql.Timestamp, 11 | scope: Option[String], clientId: String, expiresIn: Int) 12 | 13 | class AuthCodes(tag: Tag) extends Table[AuthCode](tag, "auth_codes") { 14 | def authorizationCode = column[String]("authorization_code", O.PrimaryKey) 15 | def userId = column[Int]("user_id") 16 | def redirectUri = column[Option[String]]("redirect_uri") 17 | def createdAt = column[java.sql.Timestamp]("created_at") 18 | def scope = column[Option[String]]("scope") 19 | def clientId = column[String]("client_id") 20 | def expiresIn = column[Int]("expires_in") 21 | def * = (authorizationCode, userId, redirectUri, createdAt, scope, 22 | clientId, expiresIn) <> 23 | (AuthCode.tupled, AuthCode.unapply _) 24 | } 25 | 26 | object AuthCodes { 27 | val authCodes = TableQuery[AuthCodes] 28 | val log = play.Logger.of("application") 29 | 30 | /** 31 | * Add AuthCode object to database. 32 | * @param ac 33 | * @param session 34 | * @return 35 | */ 36 | def insert(ac: AuthCode)(implicit session: Session) = { 37 | authCodes += ac 38 | } 39 | 40 | /** 41 | * Delete AuthCode object from database. 42 | * @param ac 43 | * @param session 44 | * @return 45 | */ 46 | def delete(ac: AuthCode)(implicit session: Session) = 47 | authCodes.where(_.clientId === ac.clientId).delete 48 | 49 | /** 50 | * Find AuthCode object by its value. 51 | * @param authCode 52 | * @param session 53 | * @return 54 | */ 55 | def find(authCode: String)(implicit session: Session) = { 56 | 57 | val code = authCodes.where(_.authorizationCode === authCode).firstOption 58 | 59 | log.debug(code.toString()) 60 | // filtering out expired authorization codes 61 | code.filter { p => 62 | val codeTime = p.createdAt.getTime + (p.expiresIn) 63 | val currentTime = new Date().getTime 64 | log.debug(s"codeTime: $codeTime, currentTime: $currentTime") 65 | codeTime > currentTime 66 | } 67 | } 68 | 69 | /** 70 | * Generate a new AuthCode for given client and other details. 71 | * @param clientId 72 | * @param redirectUri 73 | * @param scope 74 | * @param userId 75 | * @param expiresIn 76 | * @param session 77 | * @return 78 | */ 79 | def generateAuthCodeForClient(clientId: String, redirectUri: String, 80 | scope: String, userId: Int, expiresIn: Int)( 81 | implicit session: Session): Option[AuthCode] = { 82 | 83 | Clients.findByClientId(clientId).map { 84 | client => 85 | { 86 | val authCode = Crypto.generateAuthCode() 87 | val createdAt = new Timestamp(new Date().getTime) 88 | val redirectUriOpt = Some(redirectUri) 89 | val scopeOpt = Some(scope) 90 | val ac = AuthCode(authCode, userId, redirectUriOpt, 91 | createdAt, scopeOpt, clientId, expiresIn) 92 | 93 | // replace with new auth code 94 | delete(ac) 95 | insert(ac) 96 | ac 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/models/oauth2/Client.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | import scala.slick.lifted.Tag 5 | import java.util.Date 6 | import java.sql.Timestamp 7 | import oauth2.Crypto 8 | 9 | case class Client(id: String, username: String, secret: String, 10 | description: String, redirectUri: String, scope: String) 11 | 12 | class Clients(tag: Tag) extends Table[Client](tag, "clients") { 13 | def id = column[String]("client_id", O.PrimaryKey, O.NotNull) 14 | def username = column[String]("username", O.NotNull) 15 | def secret = column[String]("client_secret", O.NotNull) 16 | def description = column[String]("description", O.NotNull) 17 | def redirectUri = column[String]("redirect_uri", O.NotNull) 18 | def scope = column[String]("scope", O.NotNull) 19 | def * = (id, username, secret, description, redirectUri, scope) <> 20 | (Client.tupled, Client.unapply _) 21 | } 22 | 23 | object Clients { 24 | val grantTypes = TableQuery[GrantTypes] 25 | val clientGrantTypes = TableQuery[ClientGrantTypes] 26 | val clients = TableQuery[Clients] 27 | 28 | val grantTypeMapping = List( 29 | GrantType(Some(1), "authorization_code"), 30 | GrantType(Some(2), "client_credentials"), 31 | GrantType(Some(3), "password"), 32 | GrantType(Some(4), "refresh_token")) 33 | 34 | /** 35 | * Check if the given client and secret have a GrantType access configured in database. 36 | * @param clientId 37 | * @param clientSecret 38 | * @param grantType 39 | * @param session 40 | * @return 41 | */ 42 | def validate(clientId: String, clientSecret: String, grantType: String)(implicit session: Session): Boolean = { 43 | val ccgt = clients innerJoin clientGrantTypes on ((c, cgt) => c.id === cgt.clientId) 44 | val ccgtgt = (ccgt) innerJoin grantTypes on (_._2.grantTypeId === _.id) 45 | val check = for { 46 | ((c, cgt), gt) <- ccgtgt 47 | if c.id === clientId && c.secret === clientSecret && gt.grantType === grantType 48 | } yield 0 49 | check.firstOption.isDefined 50 | } 51 | 52 | /** 53 | * Fetch all clients 54 | * @param session 55 | * @return 56 | */ 57 | def list()(implicit session: Session) = { (for (c <- clients) yield c).list } 58 | 59 | /** 60 | * Fetch all Clients associated with the given username 61 | * @param username 62 | * @param session 63 | * @return 64 | */ 65 | def findByUser(username: String)(implicit session: Session) = { (for (c <- clients.filter(x => x.username === username)) yield c).list } 66 | 67 | /** 68 | * Add a new Client to database. 69 | * @param client 70 | * @param session 71 | */ 72 | def insert(client: Client)(implicit session: Session) = { 73 | clients += client 74 | updateGrantTypes(client) 75 | } 76 | 77 | /** 78 | * Update grant type for given client. 79 | * @param client 80 | * @param session 81 | */ 82 | def updateGrantTypes(client: Client)(implicit session: Session) = { 83 | clientGrantTypes.where(_.clientId === client.id).delete 84 | val cgts = grantTypeMapping.map(gt => ClientGrantType(client.id, gt.id.get)) 85 | cgts foreach { cgt => ClientGrantTypes.insert(cgt) } 86 | } 87 | 88 | /** 89 | * Delete the the given client from database. 90 | * @param client 91 | * @param session 92 | * @return 93 | */ 94 | def delete(client: Client)(implicit session: Session) = 95 | clients.where(_.id === client.id).delete 96 | 97 | /** 98 | * Update the given client. 99 | * @param client 100 | * @param session 101 | */ 102 | def update(client: Client)(implicit session: Session) = { 103 | clients.where(_.id === client.id).update(client) 104 | updateGrantTypes(client) 105 | } 106 | 107 | /** 108 | * Fetch a Client by ID 109 | * @param id 110 | * @param session 111 | * @return 112 | */ 113 | def get(id: String)(implicit session: Session): Option[Client] = 114 | clients.where(_.id === id).firstOption 115 | 116 | /** 117 | * Fetch a Client by ID 118 | * @param clientId 119 | * @param session 120 | * @return 121 | */ 122 | def findByClientId(clientId: String)(implicit session: Session): Option[Client] = 123 | clients.where(_.id === clientId).firstOption 124 | 125 | /** 126 | * Delete all clients from databse. NOTE: Use with caution. 127 | * @param session 128 | * @return 129 | */ 130 | def deleteAll()(implicit session: Session) = clients.delete 131 | 132 | } -------------------------------------------------------------------------------- /app/models/oauth2/ClientGrantType.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | import scala.slick.lifted.Tag 5 | import java.util.Date 6 | import java.sql.Timestamp 7 | import oauth2.Crypto 8 | 9 | case class ClientGrantType(clientId: String, grantTypeId: Int) 10 | 11 | class ClientGrantTypes(tag: Tag) extends Table[ClientGrantType](tag, "client_grant_types") { 12 | def clientId = column[String]("client_id") 13 | def grantTypeId = column[Int]("grant_type_id") 14 | def * = (clientId, grantTypeId) <> (ClientGrantType.tupled, ClientGrantType.unapply _) 15 | val pk = primaryKey("pk_client_grant_type", (clientId, grantTypeId)) 16 | } 17 | 18 | object ClientGrantTypes { 19 | val clientGrantTypes = TableQuery[ClientGrantTypes] 20 | 21 | def insert(cgt: ClientGrantType)(implicit session: Session) = { 22 | clientGrantTypes += cgt 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/models/oauth2/GrantType.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | import scala.slick.lifted.Tag 5 | import java.util.Date 6 | import java.sql.Timestamp 7 | import oauth2.Crypto 8 | 9 | case class GrantType(id: Option[Int], grantType: String) 10 | 11 | class GrantTypes(tag: Tag) extends Table[GrantType](tag, "grant_types") { 12 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc, O.NotNull) 13 | def grantType = column[String]("grant_type") 14 | def * = (id.?, grantType) <> (GrantType.tupled, GrantType.unapply _) 15 | } 16 | 17 | object GrantTypes { 18 | val grantTypes = TableQuery[GrantTypes] 19 | 20 | def autoInc = grantTypes returning grantTypes.map(_.id) 21 | 22 | /** 23 | * Insert Grant Type in databse table. 24 | * @param grantType 25 | * @param session 26 | * @return 27 | */ 28 | def insert(grantType: GrantType)(implicit session: Session) = { 29 | grantType.id match { 30 | case Some(id) => 31 | grantTypes += grantType 32 | case None => 33 | autoInc += grantType 34 | } 35 | } 36 | 37 | /** 38 | * Update GrantType 39 | * @param id 40 | * @param grantType 41 | * @param session 42 | * @return 43 | */ 44 | def update(id: Int, grantType: GrantType)(implicit session: Session) = 45 | grantTypes.where(_.id === grantType.id).update(grantType) 46 | } 47 | -------------------------------------------------------------------------------- /app/models/oauth2/User.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2; 2 | 3 | import play.api.db.slick.Config.driver.simple._ 4 | import scala.slick.lifted.Tag 5 | import java.util.Date 6 | import java.sql.Timestamp 7 | import oauth2.Crypto 8 | 9 | case class User(id: Option[Int], username: String, email: String, 10 | password: String, role: String) 11 | 12 | class Users(tag: Tag) extends Table[User](tag, "users") { 13 | def id = column[Int]("id", O.PrimaryKey, O.AutoInc, O.NotNull) 14 | def username = column[String]("username", O.NotNull) 15 | def email = column[String]("email", O.NotNull) 16 | def password = column[String]("password", O.NotNull) 17 | def role = column[String]("role", O.NotNull) 18 | def * = (id.?, username, email, password, role) <> (User.tupled, User.unapply _) 19 | } 20 | 21 | object Users { 22 | val users = TableQuery[Users] 23 | 24 | def get(id: Int)(implicit session: Session): Option[User] = 25 | users.where(_.id === id).firstOption 26 | 27 | def findByUsername(username: String)(implicit session: Session): Option[User] = 28 | users.where(_.username === username).firstOption 29 | 30 | /** 31 | * @param username Username to find 32 | * @param encryptedPassword Encrypted version of password 33 | * @param session Implicit database session 34 | * @return Option containing User. 35 | */ 36 | def findByUsernameAndPassword(username: String, encryptedPassword: String)(implicit session: Session): Option[User] = { 37 | users.where(user => 38 | user.username === username && user.password === encryptedPassword).firstOption 39 | } 40 | 41 | def autoInc = users returning users.map(_.id) 42 | 43 | /** 44 | * @param user User object with already encrypted password 45 | * @param session 46 | * @return 47 | */ 48 | def insert(user: User)(implicit session: Session) = { 49 | val encUser = User(user.id, user.username, user.email, user.password, user.role) 50 | encUser.id match { 51 | case None => autoInc += encUser 52 | case Some(x) => users += encUser 53 | } 54 | } 55 | 56 | /** 57 | * @param id User id to be updated 58 | * @param user New User details 59 | * @param session Implicit database session 60 | * @return 61 | */ 62 | def update(id: Int, user: User)(implicit session: Session) = 63 | users.where(_.id === user.id).update(user) 64 | 65 | /** 66 | * @param user User object to be deleted 67 | * @param session Implicit database session 68 | * @return 69 | */ 70 | def delete(user: User)(implicit session: Session) = 71 | users.where(_.id === user.id).delete 72 | 73 | /** 74 | * Delete all the users. NOTE: Use with caution. 75 | * @param session Implicit database session 76 | * @return 77 | */ 78 | def deleteAll()(implicit session: Session) = users.delete 79 | 80 | } -------------------------------------------------------------------------------- /app/oauth2/Crypto.scala: -------------------------------------------------------------------------------- 1 | package oauth2 2 | import java.util.UUID 3 | import sun.misc.BASE64Encoder 4 | import java.security.MessageDigest 5 | import java.util.Date 6 | import scala.util.Random 7 | import javax.xml.bind.DatatypeConverter 8 | 9 | object Crypto { 10 | def generateToken(): String = { 11 | val key = UUID.randomUUID.toString() 12 | new BASE64Encoder().encode(key.getBytes()) 13 | } 14 | 15 | /** 16 | * Generate an Auth Code by creating a SHA-1 digest of current date, 17 | * and random string of length 100 characters. 18 | * @return Hex encoded SHA-1 digest. 19 | */ 20 | def generateAuthCode(): String = { 21 | val md = MessageDigest.getInstance("SHA-1") 22 | val date = new Date 23 | val randomString = Random.nextString(100) 24 | md.update(date.toString().getBytes()) 25 | md.update(randomString.getBytes()) 26 | DatatypeConverter.printHexBinary(md.digest) 27 | } 28 | 29 | def generateUUID(): String = { 30 | UUID.randomUUID().toString 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/oauth2/OAuthDataHandler.scala: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import scalaoauth2.provider.{ AuthInfo, DataHandler } 4 | import play.api.db.slick.DB 5 | import play.api.Play.current 6 | import java.util.Date 7 | import java.sql.Timestamp 8 | import models.oauth2._ 9 | import models.Helpers 10 | 11 | class OAuthDataHandler extends DataHandler[User] { 12 | 13 | val log = play.Logger.of("application") 14 | 15 | def validateClient(clientId: String, clientSecret: String, grantType: String): Boolean = { 16 | log.debug(s"validateClient: $clientId $clientSecret ") 17 | DB.withTransaction { implicit session => 18 | Clients.validate(clientId, clientSecret, grantType) 19 | } 20 | } 21 | 22 | def findUser(username: String, password: String): Option[User] = { 23 | log.debug("findUser") 24 | DB.withSession { implicit session => 25 | val encodedPass = Helpers.encodePassword(password) 26 | Users.findByUsernameAndPassword(username, encodedPass) 27 | } 28 | } 29 | 30 | def createAccessToken(authInfo: AuthInfo[User]): scalaoauth2.provider.AccessToken = { 31 | log.debug("createAccessToken") 32 | DB.withSession { implicit session => 33 | val accessTokenExpiresIn = 60 * 60 // 1 hour 34 | val now = new Date() 35 | val createdAt = new Timestamp(now.getTime) 36 | val refreshToken = Crypto.generateToken() 37 | val accessToken = Crypto.generateToken() 38 | val scope = authInfo.scope 39 | val uId = authInfo.user.id.get 40 | val clientId = authInfo.clientId 41 | val client = Clients.findByClientId(clientId) 42 | client match { 43 | case None => throw new UnsupportedOperationException 44 | case Some(c) => 45 | val tokenObject = AccessToken(None, accessToken, 46 | refreshToken, 47 | c.id, 48 | uId, 49 | scope match { case None => "" case Some(s) => s }, 50 | accessTokenExpiresIn, 51 | createdAt) 52 | 53 | log.debug(tokenObject.toString()) 54 | AccessTokens.updateByUserAndClient(tokenObject, authInfo.user.id.get, c.id) 55 | 56 | scalaoauth2.provider.AccessToken(accessToken, Some(refreshToken), 57 | authInfo.scope, Some(accessTokenExpiresIn.toLong), now) 58 | } 59 | 60 | } 61 | } 62 | 63 | def getStoredAccessToken(authInfo: AuthInfo[User]): Option[scalaoauth2.provider.AccessToken] = { 64 | log.debug("getStoredAccessToken") 65 | DB.withSession { implicit session => 66 | val clientId = authInfo.clientId 67 | val client = Clients.findByClientId(clientId) 68 | client match { 69 | case None => throw new UnsupportedOperationException 70 | case Some(c) => 71 | val uid = authInfo.user.id 72 | AccessTokens.findByUserAndClient(uid.get, c.id) map { a => 73 | scalaoauth2.provider.AccessToken(a.accessToken, 74 | Some(a.refreshToken), Some(a.scope), 75 | Some(a.expiresIn.toLong), a.createdAt) 76 | } 77 | } 78 | } 79 | } 80 | 81 | def refreshAccessToken(authInfo: AuthInfo[User], refreshToken: String): scalaoauth2.provider.AccessToken = { 82 | log.debug("refreshAccessToken") 83 | createAccessToken(authInfo) 84 | } 85 | 86 | def findAuthInfoByCode(code: String): Option[AuthInfo[User]] = { 87 | log.debug("findAuthInfoByCode: " + code) 88 | DB.withSession { implicit session => 89 | AuthCodes.find(code) map { a => 90 | log.debug("found!") 91 | val user = Users.get(a.userId).get 92 | AuthInfo(user, a.clientId, a.scope, a.redirectUri) 93 | } 94 | } 95 | } 96 | 97 | def findAuthInfoByRefreshToken(refreshToken: String): Option[AuthInfo[User]] = { 98 | log.debug("findAuthInfoByRefreshToken") 99 | DB.withSession { implicit session => 100 | AccessTokens.findByRefreshToken(refreshToken) map { a => 101 | val user = Users.get(a.userId).get 102 | val client = Clients.get(a.clientId).get 103 | AuthInfo(user, client.id, Some(a.scope), Some("")) 104 | } 105 | } 106 | } 107 | 108 | def findClientUser(clientId: String, clientSecret: String, scope: Option[String]): Option[User] = { 109 | log.debug("findClientUser") 110 | None // TODO: ? 111 | } 112 | 113 | def findAccessToken(token: String): Option[scalaoauth2.provider.AccessToken] = { 114 | log.debug("findAccessToken") 115 | DB.withSession { implicit session => 116 | AccessTokens.find(token) map { a => 117 | val user = Users.get(a.userId).get 118 | val client = Clients.get(a.clientId).get 119 | scalaoauth2.provider.AccessToken(a.accessToken, Some(a.refreshToken), 120 | Some(a.scope), Some(a.expiresIn.toLong), a.createdAt) 121 | } 122 | } 123 | } 124 | 125 | def findAuthInfoByAccessToken(accessToken: scalaoauth2.provider.AccessToken): Option[AuthInfo[User]] = { 126 | log.debug("findAuthInfoByAccessToken") 127 | DB.withSession { implicit session => 128 | AccessTokens.find(accessToken.token) map { a => 129 | val user = Users.get(a.userId).get 130 | val client = Clients.get(a.clientId).get 131 | AuthInfo(user, client.id, Some(a.scope), Some("")) 132 | } 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/views/apis.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String, user: models.oauth2.User) 2 | 3 | @main("Welcome to Application Listing", user) { 4 | 5 |
Following app has requested access to your data.
8 | 9 | @helper.form(action = routes.Apps.send_auth) { 10 | 11 |@error.message
10 | } 11 | 12 | @helper.form(action = routes.Clients.update()) { 13 | @helper.inputText(clientForm("id"), 'size -> 40, 'disabled -> "disabled") 14 | @helper.inputText(clientForm("secret"), 'size -> 40, 'disabled -> "disabled") 15 | 16 | 17 | @helper.inputText(clientForm("description"), 'size -> 100) 18 | @helper.inputText(clientForm("redirectUri"), 'size -> 100) 19 | @helper.inputText(clientForm("scope"), 'size -> 40) 20 | 21 | 22 | 23 | 24 | Cancel 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/views/clients/list.scala.html: -------------------------------------------------------------------------------- 1 | @(clients: List[oauth2.Client], user: models.oauth2.User) 2 | 3 | @main("List of Clients", user) { 4 | 5 |ID | 13 |SECRET | 14 |Description | 15 |Redirect URI | 16 |Scope | 17 |Action | 18 ||
---|---|---|---|---|---|---|
@client.id | 24 |@client.secret | 25 |@client.description | 26 |@client.redirectUri | 27 |@client.scope | 28 |Edit | 29 |Delete | 30 |
@error.message
11 | } 12 | 13 | @helper.form(action = routes.Clients.add()) { 14 | @helper.inputText(clientForm("id"), 'size -> 40, 'disabled -> "disabled") 15 | @helper.inputText(clientForm("secret"), 'size -> 40, 'disabled -> "disabled") 16 | 17 | 18 | @helper.inputText(clientForm("description"), 'size -> 100) 19 | @helper.inputText(clientForm("redirectUri"), 'size -> 100) 20 | @helper.inputText(clientForm("scope"), 'size -> 40) 21 | 22 | 23 | Cancel 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/views/clients/show_client.scala.html: -------------------------------------------------------------------------------- 1 | @(client: models.oauth2.Client, user: models.oauth2.User) 2 | 3 | @main("Welcome to Apps", user) { 4 | show client 5 | 6 |Please enter your credential
33 | @form.globalError.map { error => 34 |35 | @error.message 36 |
37 | } 38 | 39 | @flash.get("success").map { message => 40 |41 | @message 42 |
43 | } 44 | 45 |54 | 55 | 56 |
57 | 58 |59 | Don't have an account? Sign up now 60 |
61 |6 | This is a sample to show how we can access API using OAuth2.0 protocol. 7 |
8 |9 | 10 | 11 | 12 |
13 |14 | (Connect to Resource Server) 15 |
16 | 17 | 28 |