├── .gitignore ├── LICENSE ├── README.md ├── activator.properties ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── src ├── main │ ├── resources │ │ ├── application.conf │ │ └── logback.xml │ └── scala │ │ ├── Boot.scala │ │ ├── persistence │ │ ├── dals │ │ │ ├── AccountsDal.scala │ │ │ ├── BaseDal.scala │ │ │ ├── OAuthAuthorizationCodesDal.scala │ │ │ ├── OAuthAuthorizationTokensDal.scala │ │ │ └── OAuthClientsDal.scala │ │ ├── entities │ │ │ ├── Account.scala │ │ │ ├── BaseEntity.scala │ │ │ ├── BaseTable.scala │ │ │ ├── OAuthAccessToken.scala │ │ │ ├── OAuthAuthorizationCode.scala │ │ │ ├── OAuthClient.scala │ │ │ └── SlickTables.scala │ │ └── handlers │ │ │ └── OAuth2DataHandler.scala │ │ ├── rest │ │ ├── OAuth2RouteProvider.scala │ │ └── OAuthRoutes.scala │ │ └── utils │ │ ├── ActorModule.scala │ │ ├── ConfigurationModule.scala │ │ └── PersistenceModule.scala └── test │ └── scala │ └── rest │ ├── AbstractRestTest.scala │ └── RoutesSpec.scala └── tutorial └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | .idea/* 9 | dist/* 10 | target/ 11 | lib_managed/ 12 | src_managed/ 13 | project/boot/ 14 | project/plugins/project/ 15 | .DS_Store 16 | # Scala-IDE specific 17 | .scala_dependencies 18 | .worksheet 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slick-akka-http-oauth2 2 | The Slick Akka Http Oauth2 is a template for using akka-http with slick, with scala-oauth2 integrated! 3 | 4 | 5 | ##Running 6 | 7 | ``` 8 | $ sbt run 9 | ``` 10 | 11 | ##Testing 12 | 13 | ``` 14 | $ sbt test 15 | ``` 16 | 17 | ##Simple examples 18 | 19 | ###Getting a token 20 | 21 | ``` 22 | $ curl http://localhost:8080/oauth/access_token -X POST -d "client_id=bob_client_id" -d "client_secret=bob_client_secret" -d "grant_type=client_credentials" 23 | ``` 24 | 25 | ``` 26 | { "token_type":"Bearer", 27 | "access_token":"sXX7cCHBsk5Qdyh2lbGfduKutgeCJb8lxZZltfpT", 28 | "expires_in":3599, 29 | "refresh_token":"hUbAKiQEN95CNtRTLHmAanqf5bZoLRkVSqctjW6m" 30 | } 31 | ``` 32 | 33 | ###Accessing protected resources 34 | 35 | ``` 36 | $ curl --dump-header - -H "Authorization: Bearer sXX7cCHBsk5Qdyh2lbGfduKutgeCJb8lxZZltfpT" http://localhost:8080/resources 37 | ``` 38 | 39 | ``` 40 | HTTP/1.1 200 OK 41 | Server: akka-http/2.4.2 42 | Date: Sun, 03 Jul 2016 21:18:26 GMT 43 | Content-Type: text/plain; charset=UTF-8 44 | Content-Length: 19 45 | 46 | Hello bob_client_id 47 | ``` 48 | 49 | ##Another examples 50 | 51 | There are examples in tests for getting tokens with authorization code and user password. 52 | 53 | ##TODO 54 | I did TDD to make the upper layer, but since I imported the dals from my play-slick-oauth2 template, the unit tests for dals are still TODO. 55 | -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | name=slick-akka-http-oauth2 2 | title= Slick Akka HTTP with oauth2 3 | description=A starter akka-http and slick app with oauth2 4 | tags=slick,akka,scala,async,oauth2 5 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | 2 | version := "0.0.1" 3 | 4 | scalaVersion := "2.11.7" 5 | 6 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8") 7 | 8 | libraryDependencies ++= { 9 | val akkaV = "2.4.2" 10 | val sprayV = "1.3.3" 11 | val slickV = "3.1.1" 12 | Seq( 13 | "com.typesafe.akka" %% "akka-http-experimental" % akkaV, 14 | "com.typesafe.akka" %% "akka-http-core" % akkaV, 15 | "com.typesafe.akka" %% "akka-http-testkit" % akkaV, 16 | "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaV, 17 | "com.typesafe.akka" %% "akka-actor" % akkaV, 18 | "com.typesafe.akka" %% "akka-testkit" % akkaV % "test", 19 | "joda-time" % "joda-time" % "2.9.4", 20 | "org.joda" % "joda-convert" % "1.8", 21 | "org.specs2" %% "specs2-core" % "2.3.11" % "test", 22 | "org.specs2" %% "specs2-mock" % "2.3.11" , 23 | "org.scalatest" % "scalatest_2.11" % "2.2.1" % "test", 24 | "junit" % "junit" % "4.11" % "test", 25 | "com.typesafe.slick" %% "slick" % slickV, 26 | "com.typesafe.slick" %% "slick-hikaricp" % slickV, 27 | "com.typesafe" % "config" % "1.2.1", 28 | "com.h2database" % "h2" % "1.3.175", 29 | "org.postgresql" % "postgresql" % "9.3-1100-jdbc41", 30 | "com.nulab-inc" %% "scala-oauth2-core" % "0.17.2", 31 | "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0", 32 | "ch.qos.logback" % "logback-classic" % "1.1.3", 33 | "org.slf4j" % "slf4j-nop" % "1.6.4" 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") 2 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = INFO 3 | } 4 | 5 | spray.can.server { 6 | request-timeout = 1s 7 | } 8 | 9 | 10 | h2db { 11 | driver = "slick.driver.H2Driver$" 12 | db { 13 | url = "jdbc:h2:mem:test1" 14 | driver = org.h2.Driver 15 | keepAliveConnection = true 16 | numThreads = 10 17 | } 18 | } 19 | 20 | h2test { 21 | driver = "slick.driver.H2Driver$" 22 | db { 23 | url = "jdbc:h2:mem:testing" 24 | driver = org.h2.Driver 25 | } 26 | } 27 | 28 | pgdb { 29 | driver = "slick.driver.PostgresDriver$" 30 | db { 31 | url = "jdbc:postgresql:test1" 32 | driver = org.postgresql.Driver 33 | user="postgres" 34 | password="postgres" 35 | numThreads = 10 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | System.out 6 | 7 | %date{MM/dd HH:mm:ss} %-5level[%.15thread] %logger{1} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/scala/Boot.scala: -------------------------------------------------------------------------------- 1 | import java.sql.Timestamp 2 | 3 | import akka.http.scaladsl.Http 4 | import akka.http.scaladsl.server.RouteConcatenation 5 | import akka.stream.ActorMaterializer 6 | import org.joda.time.DateTime 7 | import persistence.entities.{Account, OAuthClient} 8 | import rest.OAuthRoutes 9 | import utils._ 10 | 11 | object Main extends App with RouteConcatenation { 12 | // configuring modules for application, cake pattern for DI 13 | val modules = new ConfigurationModuleImpl with ActorModuleImpl with PersistenceModuleImpl 14 | implicit val system = modules.system 15 | implicit val materializer = ActorMaterializer() 16 | implicit val ec = modules.system.dispatcher 17 | 18 | modules.generateDDL() 19 | 20 | for { 21 | createAccounts <- modules.accountsDal.insert(Seq( 22 | Account(0, "bob@example.com", "48181acd22b3edaebc8a447868a7df7ce629920a", new Timestamp(new DateTime().getMillis)) // password:bob 23 | )) 24 | createOauthClients <- modules.oauthClientsDal.insert(Seq( 25 | OAuthClient(0, 1, "client_credentials", "bob_client_id", "bob_client_secret", Some("redirectUrl"), new Timestamp(new DateTime().getMillis)))) 26 | } yield { 27 | println(s"Database initialized with default values for bob and alice") 28 | } 29 | 30 | val bindingFuture = Http().bindAndHandle( 31 | new OAuthRoutes(modules).routes, "localhost", 8080) 32 | 33 | println(s"Server online at http://localhost:8080/") 34 | 35 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/dals/AccountsDal.scala: -------------------------------------------------------------------------------- 1 | package persistence.dals 2 | 3 | import java.security.MessageDigest 4 | 5 | import persistence.entities.Account 6 | import persistence.entities.SlickTables.AccountsTable 7 | import slick.driver.H2Driver.api._ 8 | import slick.driver.JdbcProfile 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.Future 12 | 13 | trait AccountsDal extends BaseDalImpl[AccountsTable,Account] { 14 | def authenticate(email: String, password: String): Future[Option[Account]] 15 | def findByAccountId(id : Long) : Future[Option[Account]] = findById(id) 16 | } 17 | 18 | class AccountsDalImpl()(implicit override val db: JdbcProfile#Backend#Database) extends AccountsDal { 19 | private def digestString(s: String): String = { 20 | val md = MessageDigest.getInstance("SHA-1") 21 | md.update(s.getBytes) 22 | md.digest.foldLeft("") { (s, b) => 23 | s + "%02x".format(if (b < 0) b + 256 else b) 24 | } 25 | } 26 | def authenticate(email: String, password: String): Future[Option[Account]] = { 27 | val hashedPassword = digestString(password) 28 | findByFilter( acc => acc.password === hashedPassword && acc.email === email).map(_.headOption) 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/dals/BaseDal.scala: -------------------------------------------------------------------------------- 1 | package persistence.dals 2 | 3 | import persistence.entities.{BaseEntity, BaseTable} 4 | import slick.driver.JdbcProfile 5 | import slick.lifted.{CanBeQueryCondition, TableQuery} 6 | import utils.{DbModule, Profile} 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.concurrent.Future 10 | 11 | trait BaseDal[T,A] { 12 | def insert(row : A): Future[Long] 13 | def insert(rows : Seq[A]): Future[Seq[Long]] 14 | def update(row : A): Future[Int] 15 | def update(rows : Seq[A]): Future[Unit] 16 | def findById(id : Long): Future[Option[A]] 17 | def findByFilter[C : CanBeQueryCondition](f: (T) => C): Future[Seq[A]] 18 | def deleteById(id : Long): Future[Int] 19 | def deleteById(ids : Seq[Long]): Future[Int] 20 | def deleteByFilter[C : CanBeQueryCondition](f: (T) => C): Future[Int] 21 | def createTable() : Future[Unit] 22 | } 23 | 24 | class BaseDalImpl[T <: BaseTable[A], A <: BaseEntity]()(implicit val tableQ: TableQuery[T], implicit val db: JdbcProfile#Backend#Database,implicit val profile: JdbcProfile) extends BaseDal[T,A] with Profile with DbModule { 25 | 26 | import profile.api._ 27 | 28 | override def insert(row: A): Future[Long] = { 29 | insert(Seq(row)).map(_.head) 30 | } 31 | 32 | override def insert(rows: Seq[A]): Future[Seq[Long]] = { 33 | db.run(tableQ returning tableQ.map(_.id) ++= rows.filter(_.isValid)) 34 | } 35 | 36 | override def update(row: A): Future[Int] = { 37 | if (row.isValid) 38 | db.run(tableQ.filter(_.id === row.id).update(row)) 39 | else 40 | Future { 41 | 0 42 | } 43 | } 44 | 45 | override def update(rows: Seq[A]): Future[Unit] = { 46 | db.run(DBIO.seq((rows.filter(_.isValid).map(r => tableQ.filter(_.id === r.id).update(r))): _*)) 47 | } 48 | 49 | override def findById(id: Long): Future[Option[A]] = { 50 | db.run(tableQ.filter(_.id === id).result.headOption) 51 | } 52 | 53 | override def findByFilter[C: CanBeQueryCondition](f: (T) => C): Future[Seq[A]] = { 54 | db.run(tableQ.withFilter(f).result) 55 | } 56 | 57 | override def deleteById(id: Long): Future[Int] = { 58 | deleteById(Seq(id)) 59 | } 60 | 61 | override def deleteById(ids: Seq[Long]): Future[Int] = { 62 | db.run(tableQ.filter(_.id.inSet(ids)).delete) 63 | } 64 | 65 | override def deleteByFilter[C : CanBeQueryCondition](f: (T) => C): Future[Int] = { 66 | db.run(tableQ.withFilter(f).delete) 67 | } 68 | 69 | override def createTable() : Future[Unit] = { 70 | db.run(DBIO.seq(tableQ.schema.create)) 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/dals/OAuthAuthorizationCodesDal.scala: -------------------------------------------------------------------------------- 1 | package persistence.dals 2 | 3 | import java.sql.Timestamp 4 | 5 | import org.joda.time.DateTime 6 | import persistence.entities.OAuthAuthorizationCode 7 | import persistence.entities.SlickTables.OauthAuthorizationCodeTable 8 | import slick.driver.H2Driver.api._ 9 | import slick.driver.JdbcProfile 10 | 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | import scala.concurrent.Future 13 | 14 | 15 | trait OAuthAuthorizationCodesDal extends BaseDalImpl[OauthAuthorizationCodeTable,OAuthAuthorizationCode]{ 16 | def findByCode(code: String): Future[Option[OAuthAuthorizationCode]] 17 | def delete(code: String): Future[Int] 18 | } 19 | 20 | class OAuthAuthorizationCodesDalImpl()(implicit override val db: JdbcProfile#Backend#Database) extends OAuthAuthorizationCodesDal { 21 | override def findByCode(code: String): Future[Option[OAuthAuthorizationCode]] = { 22 | val expireAt = new Timestamp(new DateTime().minusMinutes(30).getMillis) 23 | findByFilter(authCode => authCode.code === code && authCode.createdAt > expireAt).map(_.headOption) 24 | } 25 | 26 | override def delete(code: String): Future[Int] = deleteByFilter(_.code === code) 27 | 28 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/dals/OAuthAuthorizationTokensDal.scala: -------------------------------------------------------------------------------- 1 | package persistence.dals 2 | 3 | import java.security.SecureRandom 4 | import java.sql.Timestamp 5 | 6 | import org.joda.time.DateTime 7 | import persistence.entities.SlickTables.OauthAccessTokenTable 8 | import persistence.entities.{Account, OAuthAccessToken, OAuthClient} 9 | import slick.driver.H2Driver.api._ 10 | import slick.driver.JdbcProfile 11 | import utils.{Configuration, PersistenceModule} 12 | 13 | import scala.concurrent.ExecutionContext.Implicits.global 14 | import scala.concurrent.Future 15 | import scala.util.Random 16 | 17 | 18 | trait OAuthAccessTokensDal extends BaseDalImpl[OauthAccessTokenTable,OAuthAccessToken]{ 19 | def create(account: Account, client: OAuthClient): Future[OAuthAccessToken] 20 | def delete(account: Account, client: OAuthClient): Future[Int] 21 | def refresh(account: Account, client: OAuthClient): Future[OAuthAccessToken] 22 | def findByAccessToken(accessToken: String): Future[Option[OAuthAccessToken]] 23 | def findByAuthorized(account: Account, clientId: String): Future[Option[OAuthAccessToken]] 24 | def findByRefreshToken(refreshToken: String): Future[Option[OAuthAccessToken]] 25 | } 26 | 27 | class OAuthAccessTokensDalImpl (modules: Configuration with PersistenceModule)(implicit override val db: JdbcProfile#Backend#Database) extends OAuthAccessTokensDal { 28 | override def create(account: Account, client: OAuthClient): Future[OAuthAccessToken] = { 29 | def randomString(length: Int) = new Random(new SecureRandom()).alphanumeric.take(length).mkString 30 | val accessToken = randomString(40) 31 | val refreshToken = randomString(40) 32 | val createdAt = new Timestamp(new DateTime().getMillis) 33 | val oauthAccessToken = new OAuthAccessToken( 34 | id = 0, 35 | accountId = account.id, 36 | oauthClientId = client.id, 37 | accessToken = accessToken, 38 | refreshToken = refreshToken, 39 | createdAt = createdAt 40 | ) 41 | insert(oauthAccessToken).map(id => oauthAccessToken.copy(id = id)) 42 | } 43 | 44 | override def delete(account: Account, client: OAuthClient): Future[Int] = { 45 | deleteByFilter( oauthToken => oauthToken.accountId === account.id && oauthToken.oauthClientId === client.id) 46 | } 47 | 48 | override def refresh(account: Account, client: OAuthClient): Future[OAuthAccessToken] = { 49 | delete(account, client) 50 | create(account, client) 51 | } 52 | 53 | override def findByAuthorized(account: Account, clientId: String): Future[Option[OAuthAccessToken]] = { 54 | val query = for { 55 | oauthClient <- modules.oauthClientsDal.tableQ 56 | token <- tableQ if oauthClient.id === token.oauthClientId && oauthClient.clientId === clientId && token.accountId === account.id 57 | } yield token 58 | db.run(query.result).map(_.headOption) 59 | } 60 | 61 | override def findByAccessToken(accessToken: String): Future[Option[OAuthAccessToken]] = { 62 | findByFilter(_.accessToken === accessToken).map(_.headOption) 63 | } 64 | 65 | override def findByRefreshToken(refreshToken: String): Future[Option[OAuthAccessToken]] = { 66 | val expireAt = new Timestamp(new DateTime().minusMonths(1).getMillis) 67 | findByFilter( token => token.refreshToken === refreshToken && token.createdAt > expireAt).map(_.headOption) 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/dals/OAuthClientsDal.scala: -------------------------------------------------------------------------------- 1 | package persistence.dals 2 | 3 | import persistence.entities.SlickTables.OauthClientTable 4 | import persistence.entities.{Account, OAuthClient} 5 | import slick.driver.H2Driver.api._ 6 | import slick.driver.JdbcProfile 7 | import utils.{Configuration, PersistenceModule} 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.concurrent.Future 11 | 12 | 13 | trait OAuthClientsDal extends BaseDalImpl[OauthClientTable,OAuthClient]{ 14 | def validate(clientId: String, clientSecret: String, grantType: String): Future[Boolean] 15 | def findByClientId(clientId: String): Future[Option[OAuthClient]] 16 | def findByClientId(clientId: Long): Future[Option[OAuthClient]] = findById(clientId) 17 | def findClientCredentials(clientId: String, clientSecret: String): Future[Option[Account]] 18 | } 19 | 20 | class OAuthClientsDalImpl(modules: Configuration with PersistenceModule)(implicit override val db: JdbcProfile#Backend#Database) extends OAuthClientsDal { 21 | override def validate(clientId: String, clientSecret: String, grantType: String): Future[Boolean] = { 22 | findByFilter(oauthClient => oauthClient.clientId === clientId && oauthClient.clientSecret === clientSecret) 23 | .map(_.headOption.map(client => grantType == client.grantType || grantType == "refresh_token") 24 | .getOrElse(false)) 25 | } 26 | override def findClientCredentials(clientId: String, clientSecret: String): Future[Option[Account]] = { 27 | for { 28 | accountId <- findByFilter(oauthClient => oauthClient.clientId === clientId && oauthClient.clientSecret === clientSecret).map(_.headOption.map(_.ownerId)) 29 | account <- modules.accountsDal.findById(accountId.get) 30 | } yield account 31 | } 32 | override def findByClientId(clientId: String): Future[Option[OAuthClient]] = { 33 | findByFilter(_.clientId === clientId).map(_.headOption) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/Account.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | import java.sql.Timestamp 4 | 5 | case class Account(id: Long, email: String, password: String, createdAt: Timestamp) extends BaseEntity 6 | -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/BaseEntity.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | trait BaseEntity { 4 | val id : Long 5 | def isValid : Boolean = true 6 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/BaseTable.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | import slick.driver.H2Driver.api._ 4 | import slick.lifted.Tag 5 | import java.sql.{Timestamp} 6 | 7 | abstract class BaseTable[T](tag: Tag, name: String) extends Table[T](tag, name) { 8 | def id = column[Long]("id", O.PrimaryKey, O.AutoInc) 9 | def createdAt = column[Timestamp]("created_at") 10 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/OAuthAccessToken.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | import java.sql.Timestamp 4 | 5 | case class OAuthAccessToken( id: Long, 6 | accountId: Long, 7 | oauthClientId: Long, 8 | accessToken: String, 9 | refreshToken: String, 10 | createdAt: Timestamp 11 | ) extends BaseEntity -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/OAuthAuthorizationCode.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | import java.sql.Timestamp 4 | 5 | case class OAuthAuthorizationCode( 6 | id: Long, 7 | accountId: Long, 8 | oauthClientId: Long, 9 | code: String, 10 | redirectUri: Option[String], 11 | createdAt: Timestamp) extends BaseEntity 12 | 13 | -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/OAuthClient.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | import java.sql.Timestamp 4 | 5 | case class OAuthClient( 6 | id: Long, 7 | ownerId: Long, 8 | grantType: String, 9 | clientId: String, 10 | clientSecret: String, 11 | redirectUri: Option[String], 12 | createdAt: Timestamp 13 | ) extends BaseEntity 14 | 15 | -------------------------------------------------------------------------------- /src/main/scala/persistence/entities/SlickTables.scala: -------------------------------------------------------------------------------- 1 | package persistence.entities 2 | 3 | import slick.driver.H2Driver.api._ 4 | 5 | object SlickTables { 6 | 7 | class AccountsTable(tag : Tag) extends BaseTable[Account](tag, "accounts") { 8 | def email = column[String]("email") 9 | def password = column[String]("password") 10 | def * = (id, email, password, createdAt) <> (Account.tupled, Account.unapply) 11 | } 12 | 13 | implicit val accountsTableQ : TableQuery[AccountsTable] = TableQuery[AccountsTable] 14 | 15 | class OauthClientTable(tag : Tag) extends BaseTable[OAuthClient](tag,"oauth_clients") { 16 | def ownerId = column[Long]("owner_id") 17 | def grantType = column[String]("grant_type") 18 | def clientId = column[String]("client_id") 19 | def clientSecret = column[String]("client_secret") 20 | def redirectUri = column[Option[String]]("redirect_uri") 21 | def * = (id, ownerId, grantType, clientId, clientSecret, redirectUri, createdAt) <> (OAuthClient.tupled, OAuthClient.unapply) 22 | 23 | def owner = foreignKey( 24 | "oauth_client_account_fk", 25 | ownerId, 26 | accountsTableQ)(_.id) 27 | } 28 | 29 | implicit val OauthClientTableQ : TableQuery[OauthClientTable] = TableQuery[OauthClientTable] 30 | 31 | class OauthAuthorizationCodeTable(tag : Tag) extends BaseTable[OAuthAuthorizationCode](tag,"oauth_authorization_codes") { 32 | def accountId = column[Long]("account_id") 33 | def oauthClientId = column[Long]("oauth_client_id") 34 | def code = column[String]("code") 35 | def redirectUri = column[Option[String]]("redirect_uri") 36 | def * = (id, accountId, oauthClientId, code, redirectUri, createdAt) <> (OAuthAuthorizationCode.tupled, OAuthAuthorizationCode.unapply) 37 | 38 | def account = foreignKey( 39 | "oauth_authorization_code_account_fk", 40 | accountId, 41 | accountsTableQ)(_.id) 42 | 43 | def oauthClient = foreignKey( 44 | "oauth_authorization_code_client_fk", 45 | oauthClientId, 46 | OauthClientTableQ)(_.id) 47 | } 48 | 49 | implicit val OauthAuthorizationCodeTableQ : TableQuery[OauthAuthorizationCodeTable] = TableQuery[OauthAuthorizationCodeTable] 50 | 51 | class OauthAccessTokenTable(tag : Tag) extends BaseTable[OAuthAccessToken](tag,"oauth_access_tokens") { 52 | def accountId = column[Long]("account_id") 53 | def oauthClientId = column[Long]("oauth_client_id") 54 | def accessToken = column[String]("access_token") 55 | def refreshToken = column[String]("refresh_token") 56 | def * = (id, accountId, oauthClientId, accessToken, refreshToken, createdAt) <> (OAuthAccessToken.tupled, OAuthAccessToken.unapply) 57 | 58 | def account = foreignKey( 59 | "oauth_access_token_account_fk", 60 | accountId, 61 | accountsTableQ)(_.id) 62 | 63 | def oauthClient = foreignKey( 64 | "oauth_access_token_client_fk", 65 | oauthClientId, 66 | OauthClientTableQ)(_.id) 67 | 68 | } 69 | 70 | implicit val OauthAccessTokenTableQ : TableQuery[OauthAccessTokenTable] = TableQuery[OauthAccessTokenTable] 71 | 72 | 73 | } -------------------------------------------------------------------------------- /src/main/scala/persistence/handlers/OAuth2DataHandler.scala: -------------------------------------------------------------------------------- 1 | package persitence.handlers 2 | 3 | import persistence.entities.{Account, OAuthAccessToken} 4 | import utils.{Configuration, PersistenceModule} 5 | 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.concurrent.Future 8 | import scalaoauth2.provider.{ClientCredentialsRequest, InvalidClient, PasswordRequest, _} 9 | 10 | class OAuth2DataHandler(val modules: Configuration with PersistenceModule) extends DataHandler[Account] { 11 | 12 | override def validateClient(request: AuthorizationRequest): Future[Boolean] = { 13 | request.clientCredential.fold(Future.successful(false))(clientCredential => modules.oauthClientsDal.validate(clientCredential.clientId, 14 | clientCredential.clientSecret.getOrElse(""), request.grantType)) 15 | } 16 | 17 | override def getStoredAccessToken(authInfo: AuthInfo[Account]): Future[Option[AccessToken]] = { 18 | modules.oauthAccessTokensDal.findByAuthorized(authInfo.user, authInfo.clientId.getOrElse("")).map(_.map(toAccessToken)) 19 | } 20 | 21 | private val accessTokenExpireSeconds = 3600 22 | 23 | private def toAccessToken(accessToken: OAuthAccessToken) = { 24 | AccessToken( 25 | accessToken.accessToken, 26 | Some(accessToken.refreshToken), 27 | None, 28 | Some(accessTokenExpireSeconds), 29 | accessToken.createdAt 30 | ) 31 | } 32 | 33 | override def createAccessToken(authInfo: AuthInfo[Account]): Future[AccessToken] = { 34 | authInfo.clientId.fold(Future.failed[AccessToken](new InvalidRequest())) { clientId => 35 | (for { 36 | clientOpt <- modules.oauthClientsDal.findByClientId(clientId) 37 | toAccessToken <- modules.oauthAccessTokensDal.create(authInfo.user, clientOpt.get).map(toAccessToken) if clientOpt.isDefined 38 | } yield toAccessToken).recover { case _ => throw new InvalidRequest() } 39 | } 40 | } 41 | 42 | 43 | override def findUser(request: AuthorizationRequest): Future[Option[Account]] = 44 | request match { 45 | case request: PasswordRequest => 46 | modules.accountsDal.authenticate(request.username, request.password) 47 | case request: ClientCredentialsRequest => 48 | request.clientCredential.fold(Future.failed[Option[Account]](new InvalidRequest())) { clientCredential => 49 | for { 50 | maybeAccount <- modules.oauthClientsDal.findClientCredentials( 51 | clientCredential.clientId, 52 | clientCredential.clientSecret.getOrElse("") 53 | ) 54 | } yield maybeAccount 55 | } 56 | case _ => 57 | Future.successful(None) 58 | } 59 | 60 | override def findAuthInfoByRefreshToken(refreshToken: String): Future[Option[AuthInfo[Account]]] = { 61 | modules.oauthAccessTokensDal.findByRefreshToken(refreshToken).flatMap { 62 | case Some(accessToken) => 63 | for { 64 | account <- modules.accountsDal.findByAccountId(accessToken.accountId) 65 | client <- modules.oauthClientsDal.findByClientId(accessToken.oauthClientId) 66 | } yield { 67 | Some(AuthInfo( 68 | user = account.get, 69 | clientId = Some(client.get.clientId), 70 | scope = None, 71 | redirectUri = client.get.redirectUri 72 | )) 73 | } 74 | case None => Future.failed(new InvalidRequest()) 75 | } 76 | } 77 | 78 | override def refreshAccessToken(authInfo: AuthInfo[Account], refreshToken: String): Future[AccessToken] = { 79 | authInfo.clientId.fold(Future.failed[AccessToken](new InvalidRequest())) { clientId => (for { 80 | clientOpt <- modules.oauthClientsDal.findByClientId(clientId) 81 | toAccessToken <- modules.oauthAccessTokensDal.refresh(authInfo.user, clientOpt.get).map(toAccessToken) if clientOpt.isDefined 82 | } yield toAccessToken).recover { case _ => throw new InvalidClient() } 83 | } 84 | } 85 | 86 | override def findAuthInfoByCode(code: String): Future[Option[AuthInfo[Account]]] = { 87 | modules.oauthAuthorizationCodesDal.findByCode(code).flatMap { 88 | case Some(code) => 89 | for { 90 | account <- modules.accountsDal.findByAccountId(code.accountId) 91 | client <- modules.oauthClientsDal.findByClientId(code.oauthClientId) 92 | } yield { 93 | Some(AuthInfo( 94 | user = account.get, 95 | clientId = Some(client.get.clientId), 96 | scope = None, 97 | redirectUri = client.get.redirectUri 98 | )) 99 | } 100 | case None => Future.failed(new InvalidRequest()) 101 | } 102 | } 103 | 104 | override def deleteAuthCode(code: String): Future[Unit] = modules.oauthAuthorizationCodesDal.delete(code).map(_ => {}) 105 | 106 | override def findAccessToken(token: String): Future[Option[AccessToken]] = 107 | modules.oauthAccessTokensDal.findByAccessToken(token).map(_.map(toAccessToken)) 108 | 109 | override def findAuthInfoByAccessToken(accessToken: AccessToken): Future[Option[AuthInfo[Account]]] = { 110 | modules.oauthAccessTokensDal.findByAccessToken(accessToken.token).flatMap { 111 | case Some(accessToken) => 112 | for { 113 | account <- modules.accountsDal.findByAccountId(accessToken.accountId) 114 | client <- modules.oauthClientsDal.findByClientId(accessToken.oauthClientId) 115 | } yield { 116 | Some(AuthInfo( 117 | user = account.get, 118 | clientId = Some(client.get.clientId), 119 | scope = None, 120 | redirectUri = client.get.redirectUri 121 | )) 122 | } 123 | case None => Future.failed(new InvalidRequest()) 124 | } 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/main/scala/rest/OAuth2RouteProvider.scala: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import akka.http.scaladsl.model.StatusCodes._ 4 | import akka.http.scaladsl.server.Directives 5 | import akka.http.scaladsl.server.directives.Credentials 6 | import rest.OAuth2RouteProvider.TokenResponse 7 | import spray.json.DefaultJsonProtocol 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.concurrent.Future 11 | import scala.util.{Failure, Success} 12 | import scalaoauth2.provider._ 13 | 14 | trait OAuth2RouteProvider[U] extends Directives with DefaultJsonProtocol{ 15 | import OAuth2RouteProvider.tokenResponseFormat 16 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 17 | 18 | val oauth2DataHandler : DataHandler[U] 19 | 20 | 21 | val tokenEndpoint = new TokenEndpoint { 22 | override val handlers = Map( 23 | OAuthGrantType.CLIENT_CREDENTIALS -> new ClientCredentials, 24 | OAuthGrantType.PASSWORD -> new Password, 25 | OAuthGrantType.AUTHORIZATION_CODE -> new AuthorizationCode, 26 | OAuthGrantType.REFRESH_TOKEN -> new RefreshToken 27 | ) 28 | } 29 | 30 | def grantResultToTokenResponse(grantResult : GrantHandlerResult[U]) : TokenResponse = 31 | TokenResponse(grantResult.tokenType, grantResult.accessToken, grantResult.expiresIn.getOrElse(1L), grantResult.refreshToken.getOrElse("")) 32 | 33 | def oauth2Authenticator(credentials: Credentials): Future[Option[AuthInfo[U]]] = 34 | credentials match { 35 | case p@Credentials.Provided(token) => 36 | oauth2DataHandler.findAccessToken(token).flatMap { 37 | case Some(token) => oauth2DataHandler.findAuthInfoByAccessToken(token) 38 | case None => Future.successful(None) 39 | } 40 | case _ => Future.successful(None) 41 | } 42 | 43 | def accessTokenRoute = pathPrefix("oauth") { 44 | path("access_token") { 45 | post { 46 | formFieldMap { fields => 47 | onComplete(tokenEndpoint.handleRequest(new AuthorizationRequest(Map(), fields.map(m => m._1 -> Seq(m._2))), oauth2DataHandler)) { 48 | case Success(maybeGrantResponse) => 49 | maybeGrantResponse.fold(oauthError => complete(Unauthorized), 50 | grantResult => complete(tokenResponseFormat.write(grantResultToTokenResponse(grantResult))) 51 | ) 52 | case Failure(ex) => complete(InternalServerError, s"An error occurred: ${ex.getMessage}") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | } 60 | 61 | object OAuth2RouteProvider extends DefaultJsonProtocol{ 62 | case class TokenResponse(token_type : String, access_token : String, expires_in : Long, refresh_token : String) 63 | implicit val tokenResponseFormat = jsonFormat4(TokenResponse) 64 | } -------------------------------------------------------------------------------- /src/main/scala/rest/OAuthRoutes.scala: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import akka.http.scaladsl.model.StatusCodes._ 4 | import akka.http.scaladsl.server.{Directives, Route} 5 | import persistence.entities._ 6 | import utils.{Configuration, PersistenceModule} 7 | 8 | import scalaoauth2.provider._ 9 | 10 | class OAuthRoutes(val modules: Configuration with PersistenceModule) extends Directives with OAuth2RouteProvider[Account] { 11 | 12 | override val oauth2DataHandler = modules.oauth2DataHandler 13 | 14 | def protectedRoute = path("resources") { 15 | get { 16 | authenticateOAuth2Async[AuthInfo[Account]]("realm", oauth2Authenticator) { 17 | auth => complete(OK,s"Hello ${auth.clientId.getOrElse("")}") 18 | } 19 | } 20 | } 21 | 22 | val routes: Route = accessTokenRoute ~ protectedRoute 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/utils/ActorModule.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import akka.actor.ActorSystem 4 | 5 | 6 | trait ActorModule { 7 | val system: ActorSystem 8 | } 9 | 10 | 11 | trait ActorModuleImpl extends ActorModule { 12 | this: Configuration => 13 | val system = ActorSystem("akkingslick", config) 14 | } -------------------------------------------------------------------------------- /src/main/scala/utils/ConfigurationModule.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import java.io.File 4 | 5 | import com.typesafe.config.{Config, ConfigFactory} 6 | 7 | trait Configuration { 8 | def config: Config 9 | } 10 | 11 | trait ConfigurationModuleImpl extends Configuration { 12 | private val internalConfig: Config = { 13 | val configDefaults = ConfigFactory.load(this.getClass().getClassLoader(), "application.conf") 14 | 15 | scala.sys.props.get("application.config") match { 16 | case Some(filename) => ConfigFactory.parseFile(new File(filename)).withFallback(configDefaults) 17 | case None => configDefaults 18 | } 19 | } 20 | 21 | def config = internalConfig 22 | } -------------------------------------------------------------------------------- /src/main/scala/utils/PersistenceModule.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import persistence.dals._ 4 | import persistence.entities._ 5 | import persitence.handlers.OAuth2DataHandler 6 | import slick.backend.DatabaseConfig 7 | import slick.driver.JdbcProfile 8 | 9 | import scalaoauth2.provider.DataHandler 10 | 11 | 12 | trait Profile { 13 | val profile: JdbcProfile 14 | } 15 | 16 | 17 | trait DbModule extends Profile{ 18 | val db: JdbcProfile#Backend#Database 19 | } 20 | 21 | trait PersistenceModule { 22 | val accountsDal: AccountsDal 23 | val oauthAuthorizationCodesDal: OAuthAuthorizationCodesDal 24 | val oauthClientsDal: OAuthClientsDal 25 | val oauthAccessTokensDal: OAuthAccessTokensDal 26 | val oauth2DataHandler : DataHandler[Account] 27 | def generateDDL : Unit 28 | } 29 | 30 | 31 | trait PersistenceModuleImpl extends PersistenceModule with DbModule{ 32 | this: Configuration => 33 | 34 | // use an alternative database configuration ex: 35 | // private val dbConfig : DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig("pgdb") 36 | private val dbConfig : DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig("h2db") 37 | 38 | override implicit val profile: JdbcProfile = dbConfig.driver 39 | override implicit val db: JdbcProfile#Backend#Database = dbConfig.db 40 | 41 | override val accountsDal = new AccountsDalImpl 42 | override val oauthAuthorizationCodesDal = new OAuthAuthorizationCodesDalImpl 43 | override val oauthClientsDal = new OAuthClientsDalImpl(this) 44 | override val oauthAccessTokensDal = new OAuthAccessTokensDalImpl(this) 45 | override val oauth2DataHandler = new OAuth2DataHandler(this) 46 | 47 | override def generateDDL(): Unit = { 48 | accountsDal.createTable() 49 | oauthAccessTokensDal.createTable() 50 | oauthAuthorizationCodesDal.createTable() 51 | oauthClientsDal.createTable() 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/scala/rest/AbstractRestTest.scala: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import akka.http.scaladsl.testkit.ScalatestRouteTest 4 | import com.typesafe.config.{Config, ConfigFactory} 5 | import org.scalatest.{Matchers, WordSpec} 6 | import org.specs2.mock.Mockito 7 | import persistence.dals._ 8 | import persitence.handlers.OAuth2DataHandler 9 | import utils.{ActorModule, ConfigurationModuleImpl, PersistenceModule} 10 | 11 | trait AbstractRestTest extends WordSpec with Matchers with ScalatestRouteTest with Mockito{ 12 | 13 | trait Modules extends ConfigurationModuleImpl with ActorModule with PersistenceModule { 14 | val system = AbstractRestTest.this.system 15 | 16 | override val accountsDal = mock[AccountsDal] 17 | override val oauthAuthorizationCodesDal = mock[OAuthAuthorizationCodesDal] 18 | override val oauthClientsDal = mock[OAuthClientsDal] 19 | override val oauthAccessTokensDal = mock[OAuthAccessTokensDal] 20 | override val oauth2DataHandler = new OAuth2DataHandler(this) 21 | override def config = getConfig.withFallback(super.config) 22 | } 23 | 24 | def getConfig: Config = ConfigFactory.empty(); 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala/rest/RoutesSpec.scala: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import java.sql.Timestamp 4 | 5 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 6 | import akka.http.scaladsl.model.StatusCodes._ 7 | import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken} 8 | import akka.http.scaladsl.model.{FormData, HttpEntity} 9 | import org.joda.time.DateTime 10 | import persistence.entities._ 11 | import rest.OAuth2RouteProvider.TokenResponse 12 | 13 | import scala.concurrent.Future 14 | 15 | class RoutesSpec extends AbstractRestTest { 16 | 17 | def actorRefFactory = system 18 | val modules = new Modules { 19 | override def generateDDL: Unit = {} 20 | } 21 | val oauthRoutes = new OAuthRoutes(modules) 22 | 23 | "OAuth Routes" should { 24 | "return unauthorized when trying to get a token without any credentials" in { 25 | Post("/oauth/access_token") ~> oauthRoutes.routes ~> check { 26 | handled shouldEqual true 27 | status shouldEqual Unauthorized 28 | 29 | } 30 | } 31 | 32 | "return Ok and a token when trying to get a token with valid credentials" in { 33 | val bobUser = Account(1,"bobmail@gmail.com","pass",new Timestamp(new DateTime().getMillis)) 34 | val bobToken = OAuthAccessToken(1, 1, 1, "valid token", "refresh token", new Timestamp(new DateTime().getMillis)) 35 | 36 | modules.oauthClientsDal.validate("bob_client_id","bob_client_secret","client_credentials") returns (Future(true)) 37 | modules.oauthClientsDal.findClientCredentials("bob_client_id","bob_client_secret") returns Future(Some(bobUser)) 38 | modules.oauthAccessTokensDal.findByAuthorized(bobUser, "bob_client_id") returns Future(Some(bobToken)) 39 | 40 | Post("/oauth/access_token",FormData("client_id" -> "bob_client_id", 41 | "client_secret" -> "bob_client_secret", "grant_type" -> "client_credentials")) ~> oauthRoutes.routes ~> check { 42 | handled shouldEqual true 43 | status shouldEqual OK 44 | val response = responseAs[TokenResponse] 45 | response.access_token shouldEqual "valid token" 46 | response.refresh_token shouldEqual "refresh token" 47 | response.token_type shouldEqual "Bearer" 48 | response.expires_in shouldEqual 3599 49 | } 50 | 51 | } 52 | 53 | "return Ok and a token when trying to get a token with valid password" in { 54 | val bobUser = Account(1,"bobmail@gmail.com","pass",new Timestamp(new DateTime().getMillis)) 55 | val bobToken = OAuthAccessToken(1, 1, 1, "valid token", "refresh token", new Timestamp(new DateTime().getMillis)) 56 | 57 | modules.oauthClientsDal.validate("bob_client_id","bob_client_secret","password") returns (Future(true)) 58 | modules.accountsDal.authenticate("bobmail@gmail.com","pass") returns Future(Some(bobUser)) 59 | modules.oauthAccessTokensDal.findByAuthorized(bobUser, "bob_client_id") returns Future(Some(bobToken)) 60 | 61 | Post("/oauth/access_token",FormData("client_id" -> "bob_client_id", 62 | "client_secret" -> "bob_client_secret","username" -> "bobmail@gmail.com", "password" -> "pass", "grant_type" -> "password")) ~> oauthRoutes.routes ~> check { 63 | handled shouldEqual true 64 | status shouldEqual OK 65 | val response = responseAs[TokenResponse] 66 | response.access_token shouldEqual "valid token" 67 | response.refresh_token shouldEqual "refresh token" 68 | response.token_type shouldEqual "Bearer" 69 | response.expires_in shouldEqual 3599 70 | } 71 | 72 | } 73 | 74 | "return Ok and a token when trying to get a token with valid authorization code" in { 75 | val bobUser = Account(1,"bobmail@gmail.com","pass",new Timestamp(new DateTime().getMillis)) 76 | val bobToken = OAuthAccessToken(1, 1, 1, "valid token", "refresh token", new Timestamp(new DateTime().getMillis)) 77 | 78 | modules.oauthClientsDal.validate("bob_client_id","bob_client_secret","authorization_code") returns (Future(true)) 79 | modules.oauthAuthorizationCodesDal.findByCode("bob_code") returns Future(Some(OAuthAuthorizationCode(1,1,1,"bob_code",Some("http://localhost:3000/callback"),new Timestamp(DateTime.now().getMillis)))) 80 | modules.accountsDal.findByAccountId(1) returns Future(Some(bobUser)) 81 | modules.oauthClientsDal.findByClientId(1) returns Future(Some(OAuthClient(1,1,"authorization_code","bob_client_id","bob_client_secret",Some("http://localhost:3000/callback"),new Timestamp(DateTime.now().getMillis)))) 82 | 83 | modules.oauthAccessTokensDal.findByAuthorized(bobUser, "bob_client_id") returns Future(Some(bobToken)) 84 | modules.oauthAuthorizationCodesDal.delete("bob_code") returns Future.successful(1) 85 | 86 | Post("/oauth/access_token",FormData("client_id" -> "bob_client_id", 87 | "client_secret" -> "bob_client_secret","redirect_uri" -> "http://localhost:3000/callback", "code" -> "bob_code", "grant_type" -> "authorization_code")) ~> oauthRoutes.routes ~> check { 88 | handled shouldEqual true 89 | status shouldEqual OK 90 | val response = responseAs[TokenResponse] 91 | response.access_token shouldEqual "valid token" 92 | response.refresh_token shouldEqual "refresh token" 93 | response.token_type shouldEqual "Bearer" 94 | response.expires_in shouldEqual 3599 95 | } 96 | 97 | } 98 | 99 | "return new token after refresh" in { 100 | val bobUser = Account(1,"bobmail@gmail.com","pass",new Timestamp(new DateTime().getMillis)) 101 | val bobClient = OAuthClient(1,1,"authorization_code","bob_client_id","bob_client_secret",Some("http://localhost:3000/callback"),new Timestamp(DateTime.now().getMillis)) 102 | val bobToken = OAuthAccessToken(1, 1, 1, "valid token", "refresh token", new Timestamp(new DateTime().getMillis)) 103 | 104 | modules.oauthClientsDal.validate("bob_client_id","bob_client_secret","refresh_token") returns (Future(true)) 105 | modules.oauthAccessTokensDal.findByRefreshToken("refresh token") returns Future(Some(bobToken)) 106 | modules.accountsDal.findByAccountId(1) returns Future(Some(bobUser)) 107 | modules.oauthClientsDal.findByClientId(1) returns Future(Some(bobClient)) 108 | modules.oauthClientsDal.findByClientId("bob_client_id") returns Future(Some(bobClient)) 109 | modules.oauthAccessTokensDal.refresh(bobUser, bobClient) returns Future(bobToken) 110 | 111 | Post("/oauth/access_token",FormData("client_id" -> "bob_client_id", "client_secret" -> "bob_client_secret", 112 | "refresh_token" -> "refresh token", "grant_type" -> "refresh_token")) ~> oauthRoutes.routes ~> check { 113 | handled shouldEqual true 114 | status shouldEqual OK 115 | val response = responseAs[TokenResponse] 116 | response.access_token shouldEqual "valid token" 117 | response.refresh_token shouldEqual "refresh token" 118 | response.token_type shouldEqual "Bearer" 119 | response.expires_in shouldEqual 3599 120 | } 121 | } 122 | 123 | "don't handle when trying to access resources without token" in { 124 | modules.oauthAccessTokensDal.findByAccessToken("") returns Future(None) 125 | 126 | Get("/resources") ~> oauthRoutes.routes ~> check { 127 | handled shouldEqual false 128 | } 129 | } 130 | 131 | "return unauthorized when trying to access resources without token" in { 132 | modules.oauthAccessTokensDal.findByAccessToken("invalid") returns Future(None) 133 | 134 | Get("/resources").addHeader(Authorization(OAuth2BearerToken("invalid"))) ~> oauthRoutes.routes ~> check { 135 | handled shouldEqual false 136 | } 137 | } 138 | 139 | "return authorized when trying to access resources with a valid token" in { 140 | val bobToken = OAuthAccessToken(1, 1, 1, "valid token", "refresh token", new Timestamp(new DateTime().getMillis)) 141 | val bobUser = Account(1,"bobmail@gmail.com","pass",new Timestamp(new DateTime().getMillis)) 142 | val bobClient = OAuthClient(1,1,"authorization_code","bob_client_id","bob_client_secret",Some("http://localhost:3000/callback"),new Timestamp(DateTime.now().getMillis)) 143 | 144 | modules.oauthAccessTokensDal.findByAccessToken("valid token") returns Future(Some(bobToken)) 145 | 146 | modules.accountsDal.findByAccountId(1) returns Future(Some(bobUser)) 147 | modules.oauthClientsDal.findByClientId(1) returns Future(Some(bobClient)) 148 | 149 | Get("/resources",HttpEntity("Application/json")).addHeader(Authorization(OAuth2BearerToken("valid token"))) ~> oauthRoutes.routes ~> check { 150 | handled shouldEqual true 151 | status shouldEqual OK 152 | responseAs[String] should equal("Hello bob_client_id") 153 | } 154 | } 155 | 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slick Akka Http OAuth2 5 | 6 | 7 |
8 |

Intro

9 | 10 |

The Slick Akka Http Oauth2 shows one way to integrate scala-oauth2 with akka http. 11 |

12 | 13 |

This template is based on my other template slick-akka-http which supports the following features:

14 | 15 | 23 |

Utils: Typesafe config for property management and Typesafe Scala Logging (LazyLogging)

24 |
25 | 26 |
27 | 28 |

Running

29 | 30 |

31 | The project was thought to be used as a seed for creating akka-http projects with slick, so the implementation is minimal. The model is very simple, is a supplier with a name and description. 32 |

33 | The rest api has a get and a post (json), a get for get all the suppliers, and a post to add suppliers. You should view this info in swagger with more detail. 34 |

35 | For running the project go to section run, and run it. 36 |

37 | For running the project in sbt: 38 | 39 |


 40 |     $ sbt
 41 |     > run
 42 |     
 43 |     
44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 |
52 |

Slick 3

53 |

Models as case classes and slick 3 schemas extending from a BaseEntity and BaseTable

54 | 55 |
 case class Supplier(name: String,desc: String)  extends BaseEntity 
56 | 57 | 58 |
class SuppliersTable(tag: Tag) extends BaseTable[Supplier](tag, "SUPPLIERS") {
 59 |   def name = column[String]("userID")
 60 |   def desc = column[String]("last_name")
 61 |   def * = (id, name, desc) <> (Supplier.tupled, Supplier.unapply)
 62 | }
63 | 64 | 65 |

BaseEntity and BaseTable provide Id for the class and table.

66 | 67 | 68 |

The model definition is database independent. The database type is declared in application.conf, for each database. For instance, we use an h2 database in this example with the follow configuration:

69 |

 70 |         h2db {
 71 |           driver = "slick.driver.H2Driver$"
 72 |           db {
 73 |             url = "jdbc:h2:mem:test1"
 74 |             driver = org.h2.Driver
 75 |             keepAliveConnection = true
 76 |             numThreads = 10
 77 |           }
 78 |         }
 79 |     
80 |
81 | 82 |
83 |

Cake Pattern

84 |

This seed uses cake pattern to add the possibility to use alternative implementations of some modules, for instance, in tests, the seed uses an alternative implementation for persistence. 85 |

The seed are organized in modules: ActorModule, ConfigurationModule and PersistenceModule. When we are implementing a class/actor that uses some of this dependencies, we just mix them with the modules, or put a parameter that receives the injected modules, for instance:

86 | 87 |

 88 |         class RoutesActor(modules: Configuration with PersistenceModule)
 89 |     
90 | 91 |
92 | 93 |
94 |

Generic DAL

95 |

This seed has a generic slick dal that can be used to implement a crud for an entity.

96 |

The generic DAL is used in suppliers DAL as example:

97 | 98 |

 99 |         	override val accountsDal = new BaseDalImpl[AccountsTable,Account](TableQuery[AccountsTable]) {}
100 |     
101 | 102 |
103 | 104 | 105 | --------------------------------------------------------------------------------