├── .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 |
- Generic Data Access layer, create a DAL with crud for an entity with just one line
16 | - Models as case classes and slick models, independent from database driver and profile
17 | - Multiple database types configured in properties file (h2 and postgresql for instance)
18 | - Cake pattern for DI
19 | - Spray-json to parse json
20 | - Cake pattern for DI
21 | - Tests for DALs
22 | - Tests for routes
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 |
--------------------------------------------------------------------------------