├── 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 |

API Listing

6 | 7 |

Sample API endpoints

8 |
9 | SampleAPI Status: @{val c = routes.SampleAPI.status("ID"); (c.method + " " + c.url) } 10 |
11 | Sample: Status for give ID 123 12 |
13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/views/apps/authorize.scala.html: -------------------------------------------------------------------------------- 1 | @* authorize Template File *@ 2 | @(user: models.oauth2.User, appAuthForm: Form[AppAuthInfo]) 3 | 4 | @main("Authorize App", user) { 5 | 6 | 7 |

Following app has requested access to your data.

8 | 9 | @helper.form(action = routes.Apps.send_auth) { 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @helper.inputRadioGroup(appAuthForm("accepted"), 23 | options=Seq("Y" -> "Yes", "N" -> "No"), 24 | '_label -> "Do you want to Authorize this App?", 25 | '_error -> appAuthForm("accepted").error.map( 26 | _.withMessage("Select an option"))) 27 | 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/views/clients/edit_client.scala.html: -------------------------------------------------------------------------------- 1 | @(clientForm: Form[ClientDetails], user: models.oauth2.User)(implicit flash: Flash) 2 | 3 | @main("Welcome to Apps", user) { 4 | @flash.get("error").map { errorMessage => 5 |

Oops! @errorMessage

6 | } 7 | 8 | @clientForm.globalError.map { error => 9 |

@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 |
6 | 7 |
8 |

List of Clients

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @for(client <- clients) { 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | } 32 | 33 |
IDSECRETDescriptionRedirect URIScopeAction
@client.id@client.secret@client.description@client.redirectUri@client.scopeEditDelete
34 |

Add a new App / Client

35 |
36 |
37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/views/clients/new_client.scala.html: -------------------------------------------------------------------------------- 1 | @(clientForm: Form[ClientDetails], user: models.oauth2.User)(implicit flash: Flash) 2 | 3 | @main("Welcome to Apps", user) { 4 | 5 | @flash.get("error").map { errorMessage => 6 |

Oops! @errorMessage

7 | } 8 | 9 | @clientForm.globalError.map { error => 10 |

@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 |

Client: @client.id

7 | 15 | } 16 | -------------------------------------------------------------------------------- /app/views/docs.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String, user: models.oauth2.User) 2 | 3 | @main("Welcome to Apps", user) { 4 | Placeholder for details of API Documentation. 5 | } 6 | 7 | -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String, user: models.oauth2.User) 2 | 3 | @main("Welcome to Apps", user) { 4 |
5 | 6 | Welcome to Apps. Here you can find details for 7 | 12 | 13 |

API

14 | 15 |

Sample API endpoints

16 |
17 | StatusAPI: @{val c = routes.SampleAPI.status("ID"); (c.method + " " + c.url) } 18 |
19 | Sample: StatusAPI 123 20 |
21 | 22 |

Registered Apps

23 | 24 |
25 | } 26 | -------------------------------------------------------------------------------- /app/views/login.scala.html: -------------------------------------------------------------------------------- 1 | @(form: Form[(String, String, String)])(implicit flash: Flash) 2 | 3 | 4 | 5 | 6 | 7 | 8 | Apps: Login 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | @helper.form(routes.Auth.authenticate) { 29 |
30 |
31 |

Apps

32 |

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 |
46 | 48 |
49 |
50 | 52 |
53 |

54 | 55 | 56 |

57 | 58 | 61 |
62 |
63 | 64 | } 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String, user: models.oauth2.User)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | 8 | @title 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 |
29 | 32 | 37 | @content 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "play-oauth2-server" 2 | 3 | version := "0.1-SNAPSHOT" 4 | 5 | libraryDependencies ++= Seq( 6 | jdbc, 7 | anorm, 8 | cache, 9 | "org.webjars" %% "webjars-play" % "2.2.0", 10 | "org.webjars" % "bootstrap" % "3.1.1", 11 | "com.nulab-inc" %% "play2-oauth2-provider" % "0.7.1", 12 | "com.typesafe.play" %% "play-slick" % "0.6.1", 13 | "mysql" % "mysql-connector-java" % "5.1.18", 14 | "org.xerial" % "sqlite-jdbc" % "3.7.2" 15 | ) 16 | 17 | play.Project.playScalaSettings 18 | 19 | instrumentSettings 20 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | application.secret="BtT>En8jCt69_WxupDvM>1EuWNVSZjWFUy]F@VuXZq8_W78=yV3^Ei;l1:4;e5:p" 2 | application.langs="en" 3 | application.log=INFO 4 | logger.root=ERROR 5 | logger.play=INFO 6 | logger.application=DEBUG 7 | 8 | 9 | fixtures.polulatOnStartUp=false 10 | -------------------------------------------------------------------------------- /conf/application.dev.conf: -------------------------------------------------------------------------------- 1 | include "application.conf" 2 | 3 | ## Specify the Slick Driver 4 | # Instead of slick.db.driver use db.default.slickdriver 5 | # slick.db.driver=scala.slick.driver.SQLiteDriver 6 | db.default.slickdriver=scala.slick.driver.SQLiteDriver 7 | 8 | ## Database settings 9 | db.default.driver=org.sqlite.JDBC 10 | db.default.url="jdbc:sqlite:db/oauth2server_dev_db.sqlite" 11 | 12 | # Enable Schema generation using Slick DDL 13 | slick.default="models.oauth2.*" 14 | 15 | -------------------------------------------------------------------------------- /conf/application.prod.conf: -------------------------------------------------------------------------------- 1 | include "application.conf" 2 | 3 | db.default.name=oauth2_prod_db 4 | db.default.url="jdbc:mysql://localhost/"${db.default.name}"?characterEncoding=UTF-8" 5 | 6 | # Evolutions 7 | applyEvolutions.default=true 8 | applyDownEvolutions.default=true 9 | -------------------------------------------------------------------------------- /conf/application.test.conf: -------------------------------------------------------------------------------- 1 | include "application.conf" 2 | 3 | ## Specify the Slick Driver 4 | # Instead of slick.db.driver use db.default.slickdriver 5 | # slick.db.driver=scala.slick.driver.SQLiteDriver 6 | db.default.slickdriver=scala.slick.driver.SQLiteDriver 7 | 8 | ## Database settings 9 | db.default.driver=org.sqlite.JDBC 10 | db.default.url="jdbc:sqlite:db/oauth2server_test_db.sqlite" 11 | 12 | # Enable Schema generation using Slick DDL 13 | slick.default="models.oauth2.*" 14 | 15 | fixtures.polulatOnStartUp=true 16 | -------------------------------------------------------------------------------- /conf/evolutions/default/1.sql: -------------------------------------------------------------------------------- 1 | # --- Created by Slick DDL 2 | # To stop Slick DDL generation, remove this comment and start using Evolutions 3 | 4 | # --- !Ups 5 | 6 | create table "access_tokens" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"token" VARCHAR(254) NOT NULL,"refresh_token" VARCHAR(254) NOT NULL,"client_id" VARCHAR(254) NOT NULL,"user_id" INTEGER NOT NULL,"scope" VARCHAR(254) NOT NULL,"expires_in" BIGINT NOT NULL,"created_at" TIMESTAMP NOT NULL); 7 | create table "auth_codes" ("authorization_code" VARCHAR(254) PRIMARY KEY NOT NULL,"user_id" INTEGER NOT NULL,"redirect_uri" VARCHAR(254),"created_at" TIMESTAMP NOT NULL,"scope" VARCHAR(254),"client_id" VARCHAR(254) NOT NULL,"expires_in" INTEGER NOT NULL); 8 | create table "client_grant_types" ("client_id" VARCHAR(254) NOT NULL,"grant_type_id" INTEGER NOT NULL,constraint "pk_client_grant_type" primary key("client_id","grant_type_id")); 9 | create table "clients" ("client_id" VARCHAR(254) PRIMARY KEY NOT NULL,"username" VARCHAR(254) NOT NULL,"client_secret" VARCHAR(254) NOT NULL,"description" VARCHAR(254) NOT NULL,"redirect_uri" VARCHAR(254) NOT NULL,"scope" VARCHAR(254) NOT NULL); 10 | create table "grant_types" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"grant_type" VARCHAR(254) NOT NULL); 11 | create table "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"username" VARCHAR(254) NOT NULL,"email" VARCHAR(254) NOT NULL,"password" VARCHAR(254) NOT NULL,"role" VARCHAR(254) NOT NULL); 12 | 13 | # --- !Downs 14 | 15 | drop table "access_tokens"; 16 | drop table "auth_codes"; 17 | drop table "client_grant_types"; 18 | drop table "clients"; 19 | drop table "grant_types"; 20 | drop table "users"; 21 | 22 | -------------------------------------------------------------------------------- /conf/logger.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${application.home}/logs/application.log 7 | 8 | %date - [%level] - from %logger in %thread %n%message%n%xException%n 9 | 10 | 11 | 12 | 13 | 14 | %coloredLevel - %logger{15} - %d{HH:mm:ss.SSS} - %caller{1} - %message%n%xException{5} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /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 /apis controllers.Application.apis 8 | GET /docs controllers.Application.docs 9 | 10 | # Sample API 11 | GET /sampleapi/status/:id controllers.SampleAPI.status(id: String) 12 | GET /city/:id controllers.CityAPI.findById(id: Long) 13 | GET /city1/:id controllers.CityAPI.findById1(id: Long) 14 | 15 | 16 | # Apps 17 | GET /apps/authorize/ controllers.Apps.authorize 18 | POST /apps/send_auth/ controllers.Apps.send_auth 19 | 20 | # Clients 21 | GET /clients/ controllers.Clients.list 22 | GET /client/new controllers.Clients.create 23 | POST /client/add controllers.Clients.add 24 | POST /client/update controllers.Clients.update 25 | GET /client/edit/:id controllers.Clients.edit(id: String) 26 | PUT /client/ controllers.Clients.update 27 | GET /client/:id controllers.Clients.get(id: String) 28 | DELETE /client/:id controllers.Clients.delete(id: String) 29 | 30 | 31 | # Authentication 32 | GET /login controllers.Auth.login 33 | POST /authenticate controllers.Auth.authenticate 34 | GET /logout controllers.Auth.logout 35 | 36 | # OAuth2 Routes 37 | POST /oauth2/access_token controllers.OAuth2Controller.accessToken 38 | 39 | # Map static resources from the /public folder to the /assets URL path 40 | GET /webjars/*file controllers.WebJarAssets.at(file) 41 | GET /assets/*file controllers.Assets.at(path="/public", file) 42 | -------------------------------------------------------------------------------- /docs/fixtures.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- Sqlite3 4 | 5 | -- add a user 6 | INSERT INTO users(username, email, password, role) VALUES ('user1', 'user1@localhost', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', 'all'); 7 | 8 | -- add some clients 9 | INSERT INTO clients (client_id, username, client_secret, redirect_uri, scope, description) 10 | VALUES 11 | ('client1', 'user1', 'secret1', 'http://localhost/9001', 'read write update', 'Client 1'), 12 | ('client2', 'user1', 'secret2', 'http://localhost/9001', 'read write update', 'Client 2'), 13 | ('client3', 'user1', 'secret3', 'http://localhost/9001', 'read write update', 'Client 3'), 14 | ('client4', 'user1', 'secret4', 'http://localhost/9001', 'read write update', 'Client 4') 15 | ; 16 | 17 | 18 | -- insert grant_types in grant_type configuration table 19 | INSERT INTO grant_types (id, grant_type) VALUES (1, 'authorization_code'); 20 | INSERT INTO grant_types (id, grant_type) VALUES (2, 'client_credentials'); 21 | INSERT INTO grant_types (id, grant_type) VALUES (3, 'password'); 22 | INSERT INTO grant_types (id, grant_type) VALUES (4, 'refresh_token'); 23 | 24 | -- associate some grant_types to client 25 | INSERT INTO client_grant_types(grant_type_id, client_id) 26 | VALUES (1, 'client1'), (2, 'client1'), (3, 'client1'), (4, 'client1'); 27 | 28 | 29 | -- generate a sample auth code for client1 30 | INSERT INTO auth_codes(authorization_code, user_id, redirect_uri, scope, client_id, expires_in, created_at) VALUES('authcode1', 1, 'http://localhost:9001/', 'all', 'client1', 3600, date('now')); 31 | -------------------------------------------------------------------------------- /docs/oauth2-actors.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/docs/oauth2-actors.dia -------------------------------------------------------------------------------- /docs/oauth2-actors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/docs/oauth2-actors.png -------------------------------------------------------------------------------- /docs/oauth2-actors.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Resource Owner 20 | 21 | 22 | 23 | 24 | 25 | 26 | Browser 27 | 28 | 29 | 30 | 31 | 32 | 33 | Client App 34 | 35 | 36 | 37 | Resource Owner Own's the Resource 38 | but direct access is not allowed. 39 | 40 | 41 | Resource Owner grants 42 | permission to Client App 43 | 44 | 45 | Client App can access a 46 | Resource on Behalf of 47 | Resource Owner 48 | via Resource Server 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Resource Owner is notified about the 72 | authorization exchange workflow. 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | OAuth2.0 Actors 82 | 83 | 84 | 85 | 86 | 87 | Resource Server 88 | or 89 | API Server 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Resource 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/oauth2-api-call.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/docs/oauth2-api-call.dia -------------------------------------------------------------------------------- /docs/oauth2-api-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/docs/oauth2-api-call.png -------------------------------------------------------------------------------- /docs/oauth2-api-call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | User visits Client App 16 | 17 | 18 | 19 | 20 | 21 | 22 | User does some action that would need to 23 | make an API call to the Resource Server 24 | 25 | 26 | 27 | 28 | 29 | 30 | Access Token Available? 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Yes 45 | 46 | 47 | No 48 | 49 | 50 | 51 | 52 | 53 | Make API Call using Bearer Auth Token 54 | 55 | 56 | 57 | 58 | 59 | 60 | Generate Auth Token via Auth Code Workflow 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Auth Code Workflow 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Refresh Token Workflow 93 | 94 | 95 | 96 | 97 | 98 | 99 | API Call success? 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | No 109 | 110 | 111 | Yes 112 | 113 | 114 | 115 | 116 | 117 | Invalid Token? 118 | 119 | 120 | 121 | 122 | 123 | 124 | Expired Token? 125 | 126 | 127 | 128 | Yes 129 | 130 | 131 | No 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | Yes 145 | 146 | 147 | No 148 | 149 | 150 | 151 | 152 | 153 | Some other error occurred 154 | 155 | 156 | 157 | 158 | 159 | 160 | Refresh Token Workflow 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | Show Results 183 | 184 | 185 | 186 | 187 | 188 | 189 | End 190 | 191 | 192 | 193 | 194 | 195 | 196 | Begin 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | OAuth2 API Call from Client App 226 | 227 | 228 | 229 | 230 | 231 | Workflows not described 232 | in this diagram 233 | 234 | 235 | -------------------------------------------------------------------------------- /docs/oauth2-client-registration.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/docs/oauth2-client-registration.dia -------------------------------------------------------------------------------- /docs/oauth2-client-registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/docs/oauth2-client-registration.png -------------------------------------------------------------------------------- /docs/oauth2-client-registration.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | User Logs into Apps Server 16 | 17 | 18 | 19 | 20 | 21 | 22 | User registers a Client App with REDIRECT_URI, SCOPE and other details 23 | 24 | 25 | 26 | 27 | 28 | 29 | User now has CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, and SCOPE 30 | 31 | 32 | 33 | Client App registration on the Apps Server 34 | 35 | 36 | 37 | 38 | 39 | CLIENT_ID, CLIENT_SECRET are provided to the Client App manually by the User 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Begin 62 | 63 | 64 | 65 | 66 | 67 | 68 | End 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0 2 | -------------------------------------------------------------------------------- /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("com.typesafe.play" % "sbt-plugin" % "2.2.2") 9 | 10 | 11 | resolvers += Classpaths.sbtPluginReleases 12 | 13 | addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "0.99.5.1") 14 | 15 | -------------------------------------------------------------------------------- /public/css/custom.css: -------------------------------------------------------------------------------- 1 | 2 | body{ 3 | font-family: 'Roboto', sans-serif; 4 | font-size:14px; 5 | font-weight:400; 6 | color:#666; 7 | background:#ffffff; 8 | } 9 | label { 10 | font-weight: 500; 11 | } 12 | .loginbox{ 13 | position:absolute; 14 | width:420px; 15 | height:auto; 16 | top:50%; 17 | left:50%; 18 | margin:-250px 0 0 -210px; 19 | -webkit-box-shadow: 0 1px 2px 2px rgba(160,160,160,0.25); 20 | box-shadow: 0 1px 2px 2px rgba(160,160,160,0.25); 21 | padding:10px; 22 | background:#fff; 23 | } 24 | .loginbox h1{ 25 | font-weight:100; 26 | color:#000; 27 | text-align:center; 28 | margin:0; 29 | padding-top:65px; 30 | background:url(../images/favicon.png) no-repeat center top; 31 | font-size:20px; 32 | text-indent:-999px; 33 | overflow:hidden; 34 | } 35 | 36 | .btn-primary { 37 | color: #fff; 38 | background-color: rgb(57,181,74); 39 | border-color:rgb(42,167,59); 40 | border-bottom-width:3px; 41 | text-transform:uppercase; 42 | } 43 | .title{ 44 | font-size: 16px; 45 | color: #8F2317; 46 | font-weight: 500; 47 | text-align: center; 48 | } 49 | .signup{ 50 | font-size:15px; 51 | margin:20px 0 10px; 52 | } 53 | .signup a{ 54 | font-weight:500; 55 | } -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/public/css/main.css -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/public/images/favicon.png -------------------------------------------------------------------------------- /sample-apps/sample-app1/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | /.target 17 | /bin 18 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/README: -------------------------------------------------------------------------------- 1 | This is your new Play application 2 | ===================================== 3 | 4 | This file will be packaged with your application, when using `play dist`. 5 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/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 | import play.api.cache.Cache 7 | 8 | object Global extends GlobalSettings { 9 | 10 | override def onStart(app: Application) { 11 | super.onStart(app) 12 | implicit val currentApp = app 13 | 14 | // reset cache on startup 15 | val clientId = Play.current.configuration.getString("client_id").get 16 | val clientSecret = Play.current.configuration.getString("client_secret").get 17 | Cache.set("client_id", clientId) 18 | Cache.set("client_secret", clientSecret) 19 | Cache.set("auth_code", None) 20 | Cache.set("access_token", None) 21 | Cache.set("refresh_token", None) 22 | 23 | val apiServer = Play.current.configuration.getString("api_server").get 24 | Cache.set("api_server", apiServer) 25 | 26 | val oauth2Server = Play.current.configuration.getString("oauth2_server").get 27 | Cache.set("oauth2_server", oauth2Server) 28 | 29 | val oauth2TokenUrl = Play.current.configuration.getString("oauth2_token_url").get 30 | Cache.set("oauth2_token_url", oauth2TokenUrl) 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import java.net.URLEncoder 6 | import play.api.libs.ws.WS 7 | import scala.concurrent.Future 8 | import scala.concurrent._ 9 | import scala.concurrent.duration._ 10 | import scala.util.Failure 11 | import scala.util.Success 12 | import play.api.libs.concurrent.Execution.Implicits._ 13 | import scala.collection.JavaConversions._ 14 | import play.api.cache.Cache 15 | import play.api.Play.current 16 | 17 | object Application extends Controller { 18 | val log = play.Logger.of("application") 19 | 20 | val errorCodes = Map( 21 | "access_denied" -> "Access was denied", 22 | "invalid_request" -> "Request made was not valid", 23 | "unauthorized_client" -> "Client is not authorized to perform this action", 24 | "unsupported_response_type" -> "Response type requested is not allowed", 25 | "invalid_scope" -> "Requested scope is not allowed", 26 | "server_error" -> "Server encountered an error", 27 | "temporarily_unavailable" -> "Service is temporary unavailable") 28 | 29 | /** 30 | * Generates a Base URL based on the the hostname in request 31 | * @param request 32 | * @return Base HTTPS URL String 33 | */ 34 | def myBaseUrl(implicit request: play.api.mvc.Request[play.api.mvc.AnyContent]) = { 35 | s"https://${request.host}/" 36 | } 37 | 38 | def index = Action { implicit request => 39 | log.debug("Normal flow") 40 | Ok(views.html.index("Sample Client App")) 41 | } 42 | 43 | def sampleAPIStatus(elemId: String) = Action.async { implicit request => 44 | log.debug("Called sampleAPIStatus") 45 | 46 | val apiServer = Cache.getAs[String]("api_server").get 47 | val apiUrl = s"${apiServer}sampleapi/status/${elemId}" 48 | 49 | Cache.getAs[String]("access_token") match { 50 | case None => // generate a new access token 51 | log.debug("No access token was found in cache") 52 | Future(Redirect(routes.Application.redirect)) 53 | case Some("") => 54 | log.debug("Access token is empty") 55 | Future(Redirect(routes.Application.redirect)) 56 | case Some(accessToken) => 57 | 58 | log.debug(s"Using access token: $accessToken") 59 | val authHeader = ("Authorization", s"Bearer ${accessToken}") 60 | 61 | log.debug(s"Requesting ${apiUrl} using authHeader: ${authHeader}") 62 | val rs: Future[play.api.libs.ws.Response] = 63 | WS.url(apiUrl).withHeaders(authHeader).get() 64 | 65 | log.debug(apiUrl) 66 | log.debug(rs.toString()) 67 | 68 | val result = rs.map { response => 69 | val ahcr = response.getAHCResponse 70 | val statusCode = ahcr.getStatusCode() 71 | val statusText = ahcr.getStatusText() 72 | 73 | val res = statusCode match { 74 | case 200 => // 200 OK 75 | // we got the response back 76 | Ok(response.body.toString()) 77 | case statusCode @ _ => 78 | 79 | val wwwAuthHeaders = ahcr.getHeaders("WWW-Authenticate").toList 80 | val authHeader = wwwAuthHeaders.mkString 81 | 82 | val bearerError = """Bearer error="(.*)", error_description="(.*)"""".r 83 | log.debug(s"response: ${statusCode} - ${statusText}") 84 | 85 | statusCode match { 86 | // Unknown respose type 87 | case 400 => // 400 Bad Request 88 | // we can't make such a request 89 | log.debug("You made a bad request") 90 | authHeader match { 91 | case bearerError(e, desc) => 92 | log.debug("EEE: " + (e, desc).toString) 93 | BadRequest(s"$e - $desc") 94 | case _ => BadRequest(authHeader) 95 | } 96 | 97 | case 401 => // 401 Unauthorized 98 | // the access token is not valid 99 | log.debug("You made an unauthorized request. Get an valid token.") 100 | val errors = authHeader match { 101 | case bearerError(e, desc) => 102 | log.debug("EEE: " + (e, desc).toString) 103 | s"$e - $desc" 104 | case _ => authHeader 105 | } 106 | log.debug("Redirecting for token request") 107 | Redirect(routes.Application.redirect) 108 | case _ => 109 | val msg = s"Don't know what to do with this response: ${statusCode} - ${statusText}" 110 | log.debug(msg) 111 | BadRequest(msg) 112 | } 113 | 114 | } 115 | res 116 | } 117 | 118 | result 119 | } 120 | 121 | } 122 | 123 | def showUnauthorized = Action { implicit request => 124 | Ok(views.html.index("Your new application is ready.")) 125 | } 126 | 127 | def redirect = Action { implicit request => 128 | 129 | val srvUrl = Cache.getAs[String]("oauth2_server") 130 | srvUrl match { 131 | case None => Ok("Error with configuration") 132 | case Some(resourceServerUri) => { 133 | // val myUri = "https://cc.hcpci.com/" 134 | val redirectUri = myBaseUrl + "oauth2callback" 135 | 136 | val clientId = Cache.getAs[String]("client_id").getOrElse("") 137 | 138 | // which user? 139 | val params = Map( 140 | "client_id" -> clientId, 141 | "redirect_uri" -> redirectUri, 142 | "scope" -> "all", 143 | "response_type" -> "code") 144 | 145 | val s = params.map { x => s"${x._1}=${URLEncoder.encode(x._2, "UTF-8")}" }.mkString("&") 146 | val baseUrl = s"${resourceServerUri}apps/authorize/" 147 | val url = List(baseUrl, s).mkString("?") 148 | Redirect(url) 149 | } 150 | } 151 | 152 | } 153 | 154 | def oauth2callback = Action.async { implicit request => 155 | val mp = request.queryString.map { case (k, v) => k -> v.mkString } 156 | log.debug("oauth2callback") 157 | 158 | mp.get("error") match { 159 | case Some(error) => 160 | Future(BadRequest(s"Error: ${error} -- ${errorCodes(error)}")) 161 | case None => // No problem found 162 | 163 | mp.get("code") match { 164 | case None => 165 | Future(Ok(""" 166 | Access code wasn't received. 167 | Some problem with the workflow? 168 | Perhaps error codes need to be processed""")) 169 | 170 | case Some(authCode) => 171 | 172 | // here we store the authcode 173 | Cache.set("auth_code", authCode) 174 | log.debug("Received Auth Code") 175 | 176 | // Now exchange the auth_code and other credentials for access token 177 | log.debug("Exchange credentials and authcode for token") 178 | /* 179 | // wget -d -q -O - 180 | // --post-data "grant_type=authorization_code& 181 | // client_id=client1& 182 | // client_secret=secret1& 183 | // code=authcode1& 184 | // redirect_uri=http://localhost:9001/" 185 | // http://localhost:9002/oauth2/access_token 186 | */ 187 | val oauth2TokenUrl = Cache.getAs[String]("oauth2_token_url").get 188 | val clientId = Cache.getAs[String]("client_id").getOrElse("") 189 | val clientSecret = Cache.getAs[String]("client_secret").getOrElse("") 190 | val x = routes.Application.oauth2callback.toString 191 | log.debug(x) 192 | val redirectUri = myBaseUrl + "oauth2callback"; 193 | val params = Map( 194 | "grant_type" -> "authorization_code", 195 | "client_id" -> clientId, 196 | "client_secret" -> clientSecret, 197 | "code" -> authCode, 198 | "redirect_uri" -> redirectUri) 199 | 200 | val postData = params map (x => (x._1 -> Seq(x._2))) 201 | log.debug(postData.toString) 202 | 203 | val authTokenResponse = 204 | WS.url(oauth2TokenUrl).post((postData)) 205 | 206 | val reply = authTokenResponse.map { response => 207 | val ahcr = response.getAHCResponse 208 | 209 | val sampleResponse = """ 210 | { 211 | "access_token": "ZjlmZTE5OGEtNDM5Yi00ODczLWIxYzEtOTk5M2RhNmU5MTIy", 212 | "expires_in": 3600, 213 | "refresh_token": "OTY0MzU5NmMtNTlkOC00ZmVhLTg4OTctZjYyYzk0MDU2ZGMz", 214 | "scope": "", 215 | "token_type": "Bearer" 216 | } 217 | """ 218 | 219 | ahcr.getStatusCode() match { 220 | case 200 => 221 | // we are good to go 222 | log.debug("200 response") 223 | 224 | val json = response.json 225 | val authToken = (json \ "access_token").as[String] 226 | val refreshToken = (json \ "refresh_token").as[String] 227 | val expiresIn = (json \ "expires_in").as[Int] 228 | val scope = (json \ "scope").as[String] 229 | val tokenType = (json \ "token_type").as[String] 230 | 231 | log.debug((authToken, refreshToken, expiresIn, scope, tokenType).toString) 232 | val cachekeys = List( 233 | "access_token", 234 | "refresh_token", 235 | "expires_in", 236 | "scope", 237 | "token_type") 238 | 239 | // cachekeys foreach (k => Cache.set(k, json \ k)) 240 | Cache.set("access_token", authToken) 241 | Cache.set("refresh_token", refreshToken) 242 | Cache.set("expires_in", expiresIn) 243 | Cache.set("scope", scope) 244 | Cache.set("token_type", tokenType) 245 | 246 | cachekeys map (k => k -> Cache.get(k)) foreach println 247 | Ok(response.json) 248 | case k @ _ => 249 | log.debug(s"$k response ${ahcr.getStatusText()}") 250 | 251 | // we screwed up with either auth token or some other issue 252 | BadRequest("") 253 | } 254 | } 255 | 256 | // begin token exchange 257 | // redirect back to the original URL 258 | 259 | reply 260 | } 261 | } 262 | 263 | } 264 | 265 | } 266 | 267 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main(message) { 4 |
5 |

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 |
29 | 30 | } 31 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | 8 | @title 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 |
29 | 32 | @content 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/build.sbt: -------------------------------------------------------------------------------- 1 | name := "sample-app1" 2 | 3 | version := "1.0-SNAPSHOT" 4 | 5 | libraryDependencies ++= Seq( 6 | jdbc, 7 | anorm, 8 | cache, 9 | "org.webjars" %% "webjars-play" % "2.2.0", 10 | "org.webjars" % "bootstrap" % "3.1.1" 11 | ) 12 | 13 | play.Project.playScalaSettings 14 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/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="]wfgno^y1W6cTRhEP0SZUPdO`/uJkblv=^DuSmu8l?Dq3aTgRl`ib=bN1s8@ns`e" 9 | 10 | # The application languages 11 | # ~~~~~ 12 | application.langs="en" 13 | 14 | 15 | # Root logger: 16 | logger.root=ERROR 17 | 18 | # Logger used by the framework: 19 | logger.play=INFO 20 | 21 | # Logger provided to your application: 22 | logger.application=DEBUG 23 | 24 | # OAuth2 Settings 25 | oauth2_server="http://localhost:9002/" 26 | oauth2_token_url=${oauth2_server}oauth2/access_token 27 | api_server=${oauth2_server} 28 | client_id=client1 29 | client_secret=secret1 30 | 31 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/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 | GET /querystatus/:id controllers.Application.sampleAPIStatus(id: String) 9 | 10 | # Resource Server Redirect 11 | GET /redirect controllers.Application.redirect 12 | 13 | # OAuth2 Callback 14 | GET /oauth2callback controllers.Application.oauth2callback 15 | 16 | 17 | # Map static resources from the /public folder to the /assets URL path 18 | GET /webjars/*file controllers.WebJarAssets.at(file) 19 | GET /assets/*file controllers.Assets.at(path="/public", file) 20 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0 2 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/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("com.typesafe.play" % "sbt-plugin" % "2.2.2") -------------------------------------------------------------------------------- /sample-apps/sample-app1/public/css/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/sample-apps/sample-app1/public/css/custom.css -------------------------------------------------------------------------------- /sample-apps/sample-app1/public/css/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/sample-apps/sample-app1/public/css/main.css -------------------------------------------------------------------------------- /sample-apps/sample-app1/public/js/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxdna/play-oauth2-server/304f1123fbe4d6b9ed791f161743d694c0fd5ec9/sample-apps/sample-app1/public/js/main.js -------------------------------------------------------------------------------- /sample-apps/sample-app1/test/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * Add your spec here. 10 | * You can mock out a whole application including requests, plugins etc. 11 | * For more information, consult the wiki. 12 | */ 13 | @RunWith(classOf[JUnitRunner]) 14 | class ApplicationSpec extends Specification { 15 | 16 | "Application" should { 17 | 18 | "send 404 on a bad request" in new WithApplication{ 19 | route(FakeRequest(GET, "/boum")) must beNone 20 | } 21 | 22 | "render the index page" in new WithApplication{ 23 | val home = route(FakeRequest(GET, "/")).get 24 | 25 | status(home) must equalTo(OK) 26 | contentType(home) must beSome.which(_ == "text/html") 27 | contentAsString(home) must contain ("Your new application is ready.") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sample-apps/sample-app1/test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * add your integration spec here. 10 | * An integration test will fire up a whole play application in a real (or headless) browser 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class IntegrationSpec extends Specification { 14 | 15 | "Application" should { 16 | 17 | "work from within a browser" in new WithBrowser { 18 | 19 | browser.goTo("http://localhost:" + port) 20 | 21 | browser.pageSource must contain("Your new application is ready.") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | import play.api.test._ 5 | import play.api.test.Helpers._ 6 | import models.oauth2.User 7 | import models.oauth2.Users 8 | 9 | /** 10 | * Add your spec here. 11 | * You can mock out a whole application including requests, plugins etc. 12 | * For more information, consult the wiki. 13 | */ 14 | @RunWith(classOf[JUnitRunner]) 15 | class ApplicationSpec extends Specification { 16 | 17 | "Application" should { 18 | 19 | "send 404 on a bad request" in new WithApplication { 20 | route(FakeRequest(GET, "/boum")) must beNone 21 | } 22 | 23 | "render the login page" in new WithApplication { 24 | val home = route(FakeRequest(GET, "/")).get 25 | // must redirect to login page 26 | status(home) must equalTo(SEE_OTHER) 27 | redirectLocation(home).map { location => 28 | location must equalTo("/login") 29 | val loginpage = route(FakeRequest(GET, "/login")) map { result => 30 | status(result) must equalTo(OK) 31 | contentType(result) must beSome.which(_ == "text/html") 32 | contentAsString(result) must contain("Apps: Login") 33 | } 34 | } 35 | } 36 | 37 | "render the homepage after login" in new WithApplication { 38 | val loginpage = route(FakeRequest(GET, "/login")) map { result => 39 | status(result) must equalTo(OK) 40 | contentType(result) must beSome.which(_ == "text/html") 41 | contentAsString(result) must contain("Apps: Login") 42 | 43 | // ensure that we have a user configured 44 | 45 | import play.api.db.slick.DB 46 | DB.withSession { implicit session => 47 | val plainPass = "pass14" 48 | val encodePass = models.Helpers.encodePassword(plainPass) 49 | val user = User(None, "user14", "user1@localhost", encodePass, "all") 50 | Users.insert(user) 51 | val u = models.oauth2.Users.findByUsernameAndPassword("user14", encodePass) 52 | u must beSome.which(x => x.username == "user14") 53 | u must beSome.which(x => x.password == encodePass) 54 | } 55 | 56 | val resOpt = route(FakeRequest(POST, "/authenticate").withFormUrlEncodedBody( 57 | ("username", "user14"), ("password", "pass14"), 58 | ("redirect_url", "/"))) 59 | resOpt match { 60 | case None => throw new Exception("Login failed") 61 | case Some(res) => 62 | status(res) must equalTo(SEE_OTHER) 63 | val cookiz = cookies(res) 64 | redirectLocation(res).map { location => 65 | location must equalTo("/") 66 | 67 | val hpOpt = route(FakeRequest(GET, "/").withCookies(cookiz.toList: _*)) 68 | val homepage = hpOpt map { result => 69 | status(result) must equalTo(OK) 70 | contentType(result) must beSome.which(_ == "text/html") 71 | contentAsString(result) must contain("Welcome to Apps") 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * add your integration spec here. 10 | * An integration test will fire up a whole play application in a real (or headless) browser 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class IntegrationSpec extends Specification { 14 | 15 | "Application" should { 16 | 17 | "work from within a browser" in new WithBrowser { 18 | 19 | browser.goTo("http://localhost:" + port) 20 | 21 | browser.pageSource must contain("Apps: Login") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/models/Fixtures.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import play.api._ 4 | import java.util.Date 5 | import java.sql.Timestamp 6 | import play.api.Play.current 7 | import play.api.db.slick.DB 8 | 9 | object Fixtures { 10 | import play.api.Play.current 11 | import play.api.db.slick.DB 12 | 13 | def populate: Unit = { 14 | DB.withSession { implicit session => 15 | import models.oauth2._ 16 | 17 | val user1 = User(None, "user1", "user1@localhost", "pass1", "") 18 | 19 | // Skip the fixtures if user already exists 20 | if (Users.findByUsername(user1.username).isDefined) return ; 21 | 22 | // add a user 23 | val user1Id = Users.insert(user1) 24 | 25 | // add some clients 26 | val client1Callback = "https://client.com/oauth2callback" 27 | val client1 = Client("client1", user1.username, "secret1", "this is client1", client1Callback, "all") 28 | val client2 = Client("client2", user1.username, "secret2", "this is client2", "http://localhost/9001", "") 29 | val client3 = Client("client3", user1.username, "secret3", "this is client3", "http://localhost/9001", "") 30 | val client4 = Client("client4", user1.username, "secret4", "this is client4", "http://localhost/9001", "read,write,update") 31 | val clients = List(client1, client2, client3, client4) 32 | 33 | clients foreach (c => Clients.insert(c)) 34 | 35 | // insert grant_types in grant_type configuration table 36 | val grantTypes = List( 37 | GrantType(Some(1), "authorization_code"), 38 | GrantType(Some(2), "client_credentials"), 39 | GrantType(Some(3), "password"), 40 | GrantType(Some(4), "refresh_token")) 41 | 42 | grantTypes.foreach(gt => GrantTypes.insert(gt)) 43 | 44 | // associate some grant_types to client1 45 | 46 | val cgts = grantTypes.map(gt => ClientGrantType(client1.id, gt.id.get)) 47 | cgts foreach { cgt => ClientGrantTypes.insert(cgt) } 48 | 49 | // generate a sample auth code for client1 50 | // ('authcode1', 1, 'http://localhost:9001/', 'all', 'client1', 3600); 51 | 52 | val now = new Date() 53 | val createdAt = new Timestamp(now.getTime) 54 | val ac = AuthCode("authcode1", user1Id, 55 | Some(client1Callback), createdAt, Some("all"), client1.id, 36000) 56 | AuthCodes.insert(ac) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /test/models/oauth2/UsersSpec.scala: -------------------------------------------------------------------------------- 1 | package models.oauth2 2 | 3 | import org.specs2.mutable._ 4 | import org.specs2.runner._ 5 | import org.junit.runner._ 6 | import play.api.test._ 7 | import play.api.test.Helpers._ 8 | import models.Helpers 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class UsersSpec extends Specification { 12 | 13 | import play.api.db.slick.DB 14 | 15 | "User" should { 16 | "be creatible, updatible and deletible" in new WithApplication { 17 | DB.withSession { implicit session => 18 | Users.deleteAll() 19 | 20 | val pass1 = Helpers.encodePassword("password1") 21 | val user1 = User(None, "user1", "user1@localhost", pass1, "all") 22 | 23 | val pass2 = Helpers.encodePassword("password2") 24 | val user1pass2 = User(None, "user1", "user1@localhost", pass2, "all") 25 | val user1Id = Users.insert(user1) 26 | val rv2 = Users.update(user1Id, user1pass2) 27 | val rv3 = Users.delete(user1) 28 | } 29 | } 30 | 31 | "be searchable" in new WithApplication { 32 | DB.withSession { implicit session => 33 | Users.deleteAll() 34 | val pass = Helpers.encodePassword("pass13") 35 | val user = User(None, "user13","user1@localhost", pass, "all") 36 | Users.insert(user) 37 | val u = Users.findByUsername(user.username) 38 | u.get.username must equalTo(user.username) 39 | } 40 | } 41 | 42 | "be validated" in new WithApplication { 43 | DB.withSession { implicit session => 44 | Users.deleteAll() 45 | val plainPass = "pass14" 46 | val encodePass = Helpers.encodePassword(plainPass) 47 | val user = User(None, "user14","user1@localhost", encodePass, "all") 48 | Users.insert(user) 49 | val u = Users.findByUsernameAndPassword(user.username, encodePass) 50 | u.get.username must equalTo(user.username) 51 | u.get.password must equalTo(user.password) 52 | Users.deleteAll() 53 | } 54 | } 55 | } 56 | 57 | } 58 | --------------------------------------------------------------------------------